diff --git a/.gitignore b/.gitignore index 4210a3a1..6be03663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -.classpath -.gradle -.project -.settings -bin -build +.idea/ +*.iml +target/ +/gui/hs_err_pid*.log +**/.vscode +**/.settings +**/.classpath +**/.project \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSE.Roboto.txt b/LICENSE.Roboto.txt deleted file mode 100644 index 75b52484..00000000 --- a/LICENSE.Roboto.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8b2fc31f --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# tesseract4java: Tesseract GUI + + +A graphical user interface for the [Tesseract OCR engine][tesseract]. The program has been introduced in the [Master’s +thesis “Analyses and Heuristics for the Improvement of Optical Character Recognition Results for Fraktur Texts”][thesis] +by Paul Vorbach (German). + +[tesseract]: https://github.com/tesseract-ocr/tesseract +[thesis]: http://nbn-resolving.de/urn/resolver.pl?urn:nbn:de:bvb:20-opus-106527 + + +[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LF8T2JP2YUUUE) + +## Usage +Basic usage is documented on [our wiki page](https://github.com/tesseract4java/tesseract4java/wiki/Usage) + +## Download + +Binary distributions and release notes are available in the [releases section]. + +[Releases section]: https://github.com/tesseract4java/tesseract4java/releases + + +## Screenshots + +![Preprocessing](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-preprocessing.png) + +Preprocessing view + +![Box Editor](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-box-editor.png) + +Box editor for training + +![Glyph Overview](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-glyph-overview.png) + +Glyph overview for easier detection of errors + +![Comparison View](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-comparison.png) + +Comparison view to compare the original document with the perceived result + +![Transcription View](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-evaluation.png) + +Evaluation view with a transcription field + +![ocrevalUAtion](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/ocrevaluation.png) + +[ocrevalUAtion] + +![Batch Export](https://raw.githubusercontent.com/tesseract4java/tesseract-gui/master/screenshots/gui-batch-export.png) + +Batch export functionality to handle large projects + + +## Building and running the software + +This software is written in Java and can be built using [Apache Maven]. In order to build the software you have to +follow these steps: + + 1. `git clone https://github.com/tesseract4java/tesseract4java.git` + 2. `cd tesseract4java` + 3. `git submodule init` + 4. `git submodule update` + 5. `mvn clean package -Pstandalone`. This will include the Tesseract binaries for your platform. You can manually + define the platform by providing the option `-Djavacpp.platform=[PLATFORM]` (available platforms are + `windows-x86_64`, `windows-x86`, `linux-x86_64`, `linux-x86`, and `macosx-x86_64`). + +After you've run through all steps, the directory "gui/target" will contain the file +"tesseract4java-[VERSION]-[PLATFORM].jar", which you can run by double-clicking or executing +`java -jar tesseract4java-[VERSION]-[PLATFORM].jar`. + +[Apache Maven]: https://maven.apache.org/ + +## Credits + + - This software uses the [Tesseract OCR engine][tesseract] ([APLv2.0]). + - This software uses [ocrevalUAtion] by Rafael C. Carrasco for providing + accuracy measures of the OCR results ([GPLv3]). + - This software uses the [Silk icon set][silk] by Mark James + ([famfamfam.com]) ([CC-BY-3.0]). + +[APLv2.0]: http://www.apache.org/licenses/LICENSE-2.0 +[GPLv3]: https://www.gnu.org/licenses/gpl-3.0.html +[ocrevalUAtion]: https://github.com/impactcentre/ocrevalUAtion +[silk]: http://www.famfamfam.com/lab/icons/silk/ +[famfamfam.com]: http://www.famfamfam.com/ +[CC-BY-3.0]: http://creativecommons.org/licenses/by/3.0/ + + +## License + +GPLv3 + +~~~ +tesseract4java - a graphical user interface for the Tesseract OCR engine +Copyright (C) 2014-2019 Paul Vorbach + +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 3 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, see . +~~~ diff --git a/build-releases.sh b/build-releases.sh new file mode 100644 index 00000000..5b411cac --- /dev/null +++ b/build-releases.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +mvn clean +mvn test + +mvn package -Pstandalone -Djavacpp.platform=windows-x86_64 -DskipTests +mvn package -Pstandalone -Djavacpp.platform=windows-x86 -DskipTests +mvn package -Pstandalone -Djavacpp.platform=linux-x86_64 -DskipTests +mvn package -Pstandalone -Djavacpp.platform=linux-x86 -DskipTests +mvn package -Pstandalone -Djavacpp.platform=macosx-x86_64 -DskipTests diff --git a/build.gradle b/build.gradle deleted file mode 100644 index dcfde8fc..00000000 --- a/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -apply plugin: 'java' -apply plugin: 'eclipse' - -group = 'de.vorb' - -version = '0.0.1' -sourceCompatibility = 1.7 - -jar { - manifest { - attributes 'Implementation-Title': 'Tesseract Tools GUI', - 'Implementation-Version': version - } -} - -repositories { - mavenCentral() -} - -// include all JAR files in lib -dependencies { - compile 'com.google.guava:guava:17.0' - - compile fileTree(dir: 'lib', includes: ['*.jar']) - - testCompile 'junit:junit:4.+' -} - -// Assume UTF-8 source code -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' - -// custom javadoc generation task -// will include res/overview.html -task doc(type: Javadoc) { - source = sourceSets.main.allSource - classpath = sourceSets.main.compileClasspath - options.addStringOption('overview', 'res/overview.html') -} diff --git a/gui/pom.xml b/gui/pom.xml new file mode 100644 index 00000000..d3f526fc --- /dev/null +++ b/gui/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + + de.vorb.tesseract + tesseract4java + 0.3.0-SNAPSHOT + + + gui + + + de.vorb.tesseract.gui.controller.TesseractController + + + + + com.google.guava + guava + 19.0 + + + de.vorb.tesseract + tools + + + com.twelvemonkeys.imageio + imageio-tiff + 3.4.1 + + + junit + junit + 4.13.1 + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.5.0 + + ${mainClass} + + + + + + + + standalone + + standalone + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.6 + + ${project.parent.name}-${project.version}-${javacpp.platform} + + jar-with-dependencies + + + + true + ${mainClass} + + + false + + + + assemble-all + package + + single + + + + + + + + + diff --git a/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java new file mode 100644 index 00000000..dc6a3513 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java @@ -0,0 +1,1561 @@ +package de.vorb.tesseract.gui.controller; + +import com.google.common.collect.Lists; +import de.vorb.tesseract.gui.io.BoxFileReader; +import de.vorb.tesseract.gui.io.BoxFileWriter; +import de.vorb.tesseract.gui.io.PlainTextWriter; +import de.vorb.tesseract.gui.model.ApplicationMode; +import de.vorb.tesseract.gui.model.BatchExportModel; +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.FilteredListModel; +import de.vorb.tesseract.gui.model.ImageModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.model.PageThumbnail; +import de.vorb.tesseract.gui.model.PreferencesUtil; +import de.vorb.tesseract.gui.model.ProjectModel; +import de.vorb.tesseract.gui.model.SymbolListModel; +import de.vorb.tesseract.gui.model.SymbolOrder; +import de.vorb.tesseract.gui.util.DocumentWriter; +import de.vorb.tesseract.gui.view.BoxFileModelComponent; +import de.vorb.tesseract.gui.view.EvaluationPane; +import de.vorb.tesseract.gui.view.FeatureDebugger; +import de.vorb.tesseract.gui.view.FilteredTable; +import de.vorb.tesseract.gui.view.ImageModelComponent; +import de.vorb.tesseract.gui.view.MainComponent; +import de.vorb.tesseract.gui.view.PageModelComponent; +import de.vorb.tesseract.gui.view.PreprocessingPane; +import de.vorb.tesseract.gui.view.SymbolOverview; +import de.vorb.tesseract.gui.view.TesseractFrame; +import de.vorb.tesseract.gui.view.dialogs.BatchExportDialog; +import de.vorb.tesseract.gui.view.dialogs.CharacterHistogram; +import de.vorb.tesseract.gui.view.dialogs.Dialogs; +import de.vorb.tesseract.gui.view.dialogs.ImportTranscriptionDialog; +import de.vorb.tesseract.gui.view.dialogs.NewProjectDialog; +import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog; +import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog.ResultState; +import de.vorb.tesseract.gui.view.dialogs.UnicharsetDebugger; +import de.vorb.tesseract.gui.work.BatchExecutor; +import de.vorb.tesseract.gui.work.PageListWorker; +import de.vorb.tesseract.gui.work.PageRecognitionProducer; +import de.vorb.tesseract.gui.work.PreprocessingWorker; +import de.vorb.tesseract.gui.work.RecognitionWorker; +import de.vorb.tesseract.gui.work.ThumbnailWorker; +import de.vorb.tesseract.gui.work.ThumbnailWorker.Task; +import de.vorb.tesseract.tools.preprocessing.DefaultPreprocessor; +import de.vorb.tesseract.tools.preprocessing.Preprocessor; +import de.vorb.tesseract.tools.recognition.RecognitionProducer; +import de.vorb.tesseract.tools.training.Unicharset; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; +import de.vorb.tesseract.util.TraineddataFiles; +import de.vorb.tesseract.util.feat.Feature3D; +import de.vorb.util.FileNames; +import eu.digitisation.input.Batch; +import eu.digitisation.input.Parameters; +import eu.digitisation.input.WarningException; +import eu.digitisation.output.Report; + +import javax.imageio.ImageIO; +import javax.swing.DefaultListModel; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JList; +import javax.swing.JTabbedPane; +import javax.swing.JViewport; +import javax.swing.ListModel; +import javax.swing.ListSelectionModel; +import javax.swing.ProgressMonitor; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileFilter; +import javax.xml.transform.TransformerException; +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Observable; +import java.util.Observer; +import java.util.Optional; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.TreeMap; +import java.util.prefs.Preferences; + +public class TesseractController extends WindowAdapter implements + ActionListener, ListSelectionListener, Observer, ChangeListener { + + public static void main(String[] args) { + setLookAndFeel(); + + try { + new TesseractController(); + } catch (Throwable e) { + Dialogs.showError(null, "Fatal error", + String.format("The necessary libraries could not be loaded: '%s'", e.getMessage())); + + throw e; + } + } + + private static void setLookAndFeel() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e1) { + // fail silently + try { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } catch (Exception e2) { + // fail silently + } + + // If the system LaF is not available, use whatever LaF is already + // being used. + } + } + + // constants + private static final String KEY_TRAINING_FILE = "training_file"; + private static final String KEY_BOX_FILE = "box_file"; + + public static final Preprocessor DEFAULT_PREPROCESSOR = new DefaultPreprocessor(); + + // components references + private final TesseractFrame view; + + private ApplicationMode mode = ApplicationMode.NONE; + + private final FeatureDebugger featureDebugger; + private MainComponent activeComponent; + + private final PageRecognitionProducer pageRecognitionProducer; + private Optional preprocessingWorker = Optional.empty(); + + // IO workers, timers and tasks + private Optional thumbnailLoader = Optional.empty(); + private final Timer pageSelectionTimer = new Timer("PageSelectionTimer"); + + private Optional lastPageSelectionTask = Optional.empty(); + private final Timer thumbnailLoadTimer = new Timer("ThumbnailLoadTimer"); + + private Optional lastThumbnailLoadTask = Optional.empty(); + + private final List tasks = new LinkedList<>(); + + // models + private Optional projectModel = Optional.empty(); + private Optional pageThumbnail = Optional.empty(); + + private String lastTraineddataFile; + + // preprocessing + private Preprocessor defaultPreprocessor = new DefaultPreprocessor(); + private final Map preprocessors = new HashMap<>(); + + private Set changedPreprocessors = new HashSet<>(); + + private Optional recognitionWorker = Optional.empty(); + + public TesseractController() { + // create new tesseract frame + view = new TesseractFrame(); + featureDebugger = new FeatureDebugger(view); + + setApplicationMode(ApplicationMode.NONE); + + handleActiveComponentChange(); + + final Path tessdataDir = TraineddataFiles.getTessdataDir(); + if (!Files.isReadable(tessdataDir)) { + Dialogs.showError(null, "Fatal Error", + String.format("The tessdata directory could not be read: '%s'", tessdataDir.toAbsolutePath())); + } + + pageRecognitionProducer = new PageRecognitionProducer( + this, + TraineddataFiles.getTessdataDir(), + RecognitionProducer.DEFAULT_TRAINING_FILE); + + // init traineddata files + try { + final List traineddataFiles = TraineddataFiles.getAvailable(); + + // prepare traineddata file list model + final DefaultListModel traineddataFilesModel = new DefaultListModel<>(); + + traineddataFiles.forEach(traineddataFilesModel::addElement); + + final JList traineddataFilesList = view.getTraineddataFiles().getList(); + + // wrap it in a filtered model + traineddataFilesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + traineddataFilesList.setModel(new FilteredListModel<>(traineddataFilesModel)); + + lastTraineddataFile = PreferencesUtil.getPreferences() + .get(KEY_TRAINING_FILE, RecognitionProducer.DEFAULT_TRAINING_FILE); + + traineddataFilesList.setSelectedValue(lastTraineddataFile, true); + + // handle the new traineddata file selection + handleTraineddataFileSelection(); + } catch (IOException e) { + Dialogs.showError(view, "Error", "Traineddata files could not be found."); + } + + try { + pageRecognitionProducer.init(); + pageRecognitionProducer.setPageSegmentationMode(getPageSegmentationMode()); + + } catch (IOException e) { + e.printStackTrace(); + } + + // register listeners + view.addWindowListener(this); + view.getMainTabs().addChangeListener(this); + + { + // menu + view.getMenuItemExit().addActionListener(this); + view.getMenuItemNewProject().addActionListener(this); + // view.getMenuItemOpenProject().addActionListener(this); + view.getMenuItemOpenBoxFile().addActionListener(this); + // view.getMenuItemSaveProject().addActionListener(this); + view.getMenuItemSaveBoxFile().addActionListener(this); + // view.getMenuItemSavePage().addActionListener(this); + view.getMenuItemCloseProject().addActionListener(this); + view.getMenuItemOpenProjectDirectory().addActionListener(this); + view.getMenuItemImportTranscriptions().addActionListener(this); + view.getMenuItemBatchExport().addActionListener(this); + view.getMenuItemPreferences().addActionListener(this); + view.getMenuItemCharacterHistogram().addActionListener(this); + view.getMenuItemInspectUnicharset().addActionListener(this); + view.getMenuItemTesseractTrainer().addActionListener(this); + } + + view.getPages().getList().addListSelectionListener(this); + final JViewport pagesViewport = + (JViewport) view.getPages().getList().getParent(); + pagesViewport.addChangeListener(this); + view.getTraineddataFiles().getList().addListSelectionListener(this); + view.getScale().addObserver(this); + + { + // preprocessing pane + final PreprocessingPane preprocessingPane = view.getPreprocessingPane(); + + preprocessingPane.getPreviewButton().addActionListener(this); + preprocessingPane.getApplyPageButton().addActionListener(this); + preprocessingPane.getApplyAllPagesButton().addActionListener(this); + } + + { + // glyph overview pane + final SymbolOverview symbolOverview = view.getSymbolOverview(); + symbolOverview.getSymbolGroupList().getList() + .addListSelectionListener(this); + symbolOverview.getSymbolVariantList().getList() + .addListSelectionListener(this); + symbolOverview.getSymbolVariantList().getCompareToPrototype() + .addActionListener(this); + symbolOverview.getSymbolVariantList().getShowInBoxEditor() + .addActionListener(this); + symbolOverview.getSymbolVariantList().getOrderingComboBox() + .addActionListener(this); + } + + { + // evaluation pane + final EvaluationPane evalPane = view.getEvaluationPane(); + evalPane.getSaveTranscriptionButton().addActionListener(this); + evalPane.getGenerateReportButton().addActionListener(this); + evalPane.getUseOCRResultButton().addActionListener(this); + } + + view.setVisible(true); + } + + @Override + public void actionPerformed(ActionEvent evt) { + final Object source = evt.getSource(); + final SymbolOverview symbolOverview = view.getSymbolOverview(); + final PreprocessingPane preprocessingPane = view.getPreprocessingPane(); + final EvaluationPane evalPane = view.getEvaluationPane(); + + if (source.equals(view.getMenuItemExit())) { + handleExit(); + } else if (source.equals(view.getMenuItemNewProject())) { + handleNewProject(); + // } else if (source.equals(view.getMenuItemOpenProject())) { + // handleOpenProject(); + } else if (source.equals(view.getMenuItemOpenBoxFile())) { + handleOpenBoxFile(); + // } else if (source.equals(view.getMenuItemSaveProject())) { + // handleSaveProject(); + } else if (source.equals(view.getMenuItemSaveBoxFile())) { + handleSaveBoxFile(); + } else if (source.equals(view.getMenuItemCloseProject())) { + handleCloseProject(); + } else if (source.equals(view.getMenuItemOpenProjectDirectory())) { + handleOpenProjectDirectory(); + } else if (source.equals(view.getMenuItemImportTranscriptions())) { + handleImportTranscriptions(); + } else if (source.equals(view.getMenuItemBatchExport())) { + handleBatchExport(); + } else if (source.equals(view.getMenuItemPreferences())) { + handlePreferences(); + } else if (source.equals(view.getMenuItemCharacterHistogram())) { + handleCharacterHistogram(); + } else if (source.equals(view.getMenuItemInspectUnicharset())) { + handleInspectUnicharset(); + } else if (source.equals(view.getMenuItemTesseractTrainer())) { + handleTesseractTrainer(); + } else if (preprocessingPane.getPreviewButton().equals(source)) { + handlePreprocessorPreview(); + } else if (preprocessingPane.getApplyPageButton().equals(source)) { + handlePreprocessorChange(false); + } else if (preprocessingPane.getApplyAllPagesButton().equals(source)) { + handlePreprocessorChange(true); + } else if (source.equals(symbolOverview.getSymbolVariantList().getCompareToPrototype())) { + handleCompareSymbolToPrototype(); + } else if (source.equals(symbolOverview.getSymbolVariantList().getShowInBoxEditor())) { + handleShowSymbolInBoxEditor(); + } else if (source.equals(symbolOverview.getSymbolVariantList().getOrderingComboBox())) { + handleSymbolReordering(); + } else if (source.equals(evalPane.getSaveTranscriptionButton())) { + handleTranscriptionSave(); + } else if (source.equals(evalPane.getGenerateReportButton())) { + handleGenerateReport(); + } else if (source.equals(evalPane.getUseOCRResultButton())) { + handleUseOCRResult(); + } else { + throw new UnsupportedOperationException(String.format("Unhandled ActionEvent: '%s'", evt)); + } + } + + public Optional getPageModel() { + final MainComponent active = view.getActiveComponent(); + + if (active instanceof PageModelComponent) { + return ((PageModelComponent) active).getPageModel(); + } + + return Optional.empty(); + } + + public PageRecognitionProducer getPageRecognitionProducer() { + return pageRecognitionProducer; + } + + public Optional getProjectModel() { + return projectModel; + } + + public Optional getSelectedPage() { + final PageThumbnail thumbnail = view.getPages().getList().getSelectedValue(); + + if (thumbnail == null) { + return Optional.empty(); + } else { + return Optional.of(thumbnail.getFile()); + } + } + + public Optional getTrainingFile() { + return Optional.ofNullable(view.getTraineddataFiles().getList().getSelectedValue()); + } + + public TesseractFrame getView() { + return view; + } + + private void handleActiveComponentChange() { + final MainComponent active = view.getActiveComponent(); + + // didn't change + if (active == activeComponent) { + return; + } + + if (mode == ApplicationMode.BOX_FILE) { + // if we're in box file mode, everything is simple + if (active == view.getBoxEditor()) { + view.getBoxEditor().setBoxFileModel(view.getSymbolOverview().getBoxFileModel()); + } else { + view.getSymbolOverview().setBoxFileModel(view.getBoxEditor().getBoxFileModel()); + } + } else if (mode == ApplicationMode.PROJECT) { + // in project mode, it's a bit more complicated + + if (active instanceof ImageModelComponent) { + if (activeComponent instanceof ImageModelComponent) { + // ImageModelComponent -> ImageModelComponent + setImageModel(((ImageModelComponent) activeComponent).getImageModel()); + } else if (activeComponent instanceof PageModelComponent) { + // PageModelComponent -> ImageModelComponent + final Optional pm = ((PageModelComponent) activeComponent).getPageModel(); + + if (pm.isPresent()) { + setImageModel(Optional.of(pm.get().getImageModel())); + } else { + setImageModel(Optional.empty()); + } + } else { + setImageModel(Optional.empty()); + } + } else if (active instanceof PageModelComponent) { + if (activeComponent instanceof PageModelComponent) { + // PageModelComponent -> PageModelComponent + setPageModel(((PageModelComponent) activeComponent).getPageModel()); + } else if (activeComponent instanceof ImageModelComponent) { + // ImageModelComponent -> PageModelComponent + setImageModel(((ImageModelComponent) activeComponent).getImageModel()); + } else { + setPageModel(Optional.empty()); + } + } + } + + activeComponent = active; + } + + private void handleOpenProjectDirectory() { + if (Desktop.isDesktopSupported()) { + try { + Desktop.getDesktop().browse(projectModel.get().getProjectDir().toUri()); + } catch (IOException e) { + Dialogs.showError(view, "Exception", + "Project directory could not be opened."); + } + } + } + + private void handleUseOCRResult() { + if (getPageModel().isPresent()) { + final StringWriter ocrResult = new StringWriter(); + try { + new PlainTextWriter(true).write(getPageModel().get().getPage(), ocrResult); + + view.getEvaluationPane().getTextAreaTranscript().setText(ocrResult.toString()); + } catch (IOException e) { + Dialogs.showWarning(view, "Error", "Could not use the OCR result."); + } + } + } + + private void handleImportTranscriptions() { + final ImportTranscriptionDialog importDialog = + new ImportTranscriptionDialog(); + importDialog.setVisible(true); + + if (importDialog.isApproved() && projectModel.isPresent()) { + final Path file = importDialog.getTranscriptionFile(); + final String sep = importDialog.getPageSeparator(); + + try { + Files.createDirectories(projectModel.get().getTranscriptionDir()); + + view.getProgressBar().setIndeterminate(true); + + try (final BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + + // for every file + for (final Path imgFile : projectModel.get().getImageFiles()) { + final Path filename = FileNames.replaceExtension(imgFile, "txt").getFileName(); + final Path transcription = projectModel.get().getTranscriptionDir().resolve(filename); + + final BufferedWriter writer = Files.newBufferedWriter(transcription, StandardCharsets.UTF_8); + + int lines = 0; + + // read file line by line + String line; + while ((line = reader.readLine()) != null) { + // if the line equals the separator, create the next + // file + if (line.equals(sep)) { + break; + } + + lines++; + // otherwise write the line to the current file + writer.write(line); + writer.write('\n'); + } + + // if a transcription file is empty, delete it + if (lines == 0) { + Files.delete(transcription); + } + + writer.write('\n'); + writer.close(); + } + + } + + Dialogs.showInfo(view, "Import Transcriptions", "Transcription file successfully imported."); + } catch (IOException e) { + Dialogs.showError(view, "Import Exception", "Could not import the transcription file."); + } finally { + view.getProgressBar().setIndeterminate(false); + } + } + } + + private void handleBatchExport() { + final Optional export = BatchExportDialog.showBatchExportDialog(this); + + if (export.isPresent()) { + final BatchExecutor batchExec = new BatchExecutor(this, this.getProjectModel().get(), export.get()); + + try { + final int totalFiles = + (int) Lists.newArrayList(this.getProjectModel().get().getImageFiles()).stream().count(); + + final ProgressMonitor progressMonitor = new ProgressMonitor(view, "Processing:", "", 0, totalFiles + 1); + progressMonitor.setProgress(0); + + try (final BufferedWriter errorLog = Files.newBufferedWriter( + export.get().getDestinationDir().resolve("errors.log"), + StandardCharsets.UTF_8)) { + + batchExec.start(progressMonitor, errorLog); + + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + } + + private void handleCompareSymbolToPrototype() { + final Symbol selected = view.getSymbolOverview().getSymbolVariantList().getList().getSelectedValue(); + + final Optional pm = getPageModel(); + if (pm.isPresent()) { + final BufferedImage pageImg = pm.get().getImageModel().getPreprocessedImage(); + final Box symbolBox = selected.getBoundingBox(); + final BufferedImage symbolImg = pageImg.getSubimage( + symbolBox.getX(), symbolBox.getY(), + symbolBox.getWidth(), symbolBox.getHeight()); + + final List features = pageRecognitionProducer.getFeaturesForSymbol(symbolImg); + + featureDebugger.setFeatures(features); + featureDebugger.setVisible(true); + } + } + + private void handleGenerateReport() { + final Optional transcriptionFile = handleTranscriptionSave(); + + if (!transcriptionFile.isPresent()) { + Dialogs.showWarning(view, "Report", "The report could not be generated."); + return; + } + + final Path sourceFile = getPageModel().get().getImageModel().getSourceFile(); + final Path fname = FileNames.replaceExtension(sourceFile, "txt").getFileName(); + final Path repName = FileNames.replaceExtension(fname, "html"); + final Path plain = projectModel.get().getOCRDir().resolve(fname); + final Path report = projectModel.get().getEvaluationDir().resolve(repName); + + try { + final Path equivalencesFile = prepareReports(); + + // generate report + final Batch reportBatch = new Batch(transcriptionFile.get().toFile(), plain.toFile()); + final Parameters pars = new Parameters(); + pars.eqfile.setValue(equivalencesFile.toFile()); + final Report rep = new Report(reportBatch, pars); + + // write to file + DocumentWriter.writeToFile(rep.document(), report); + + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(report.toFile()); + } + } catch (WarningException | IOException | TransformerException e) { + e.printStackTrace(); + } + } + + public Path prepareReports() throws IOException { + Files.createDirectories(projectModel.get().getEvaluationDir()); + + final Path equivalencesFile = projectModel.get().getProjectDir().resolve("character_equivalences.csv"); + + if (!Files.exists(equivalencesFile)) { + // copy the default character equivalences to the equivalences file + + try ( + final BufferedInputStream defaultEq = new BufferedInputStream( + getClass().getResourceAsStream("/default_character_equivalences.csv")); + final BufferedOutputStream eq = new BufferedOutputStream( + new FileOutputStream(equivalencesFile.toFile())) + ) { + + int c; + while ((c = defaultEq.read()) != -1) { + eq.write(c); + } + + } + } + + return equivalencesFile; + } + + private Optional handleTranscriptionSave() { + try { + if (projectModel.isPresent() && getPageModel().isPresent()) { + Files.createDirectories( + projectModel.get().getTranscriptionDir()); + + final Path sourceFile = + getPageModel().get().getImageModel().getSourceFile(); + final Path fileName = + FileNames.replaceExtension(sourceFile, "txt").getFileName(); + + final Path transcriptionFile = projectModel.get().getTranscriptionDir().resolve(fileName); + + try (final Writer writer = Files.newBufferedWriter(transcriptionFile, StandardCharsets.UTF_8)) { + + final String transcription = view.getEvaluationPane().getTextAreaTranscript().getText(); + + writer.write(transcription); + + return Optional.of(transcriptionFile); + } + } + } catch (IOException e) { + Dialogs.showError(view, "Exception", "Transcription could not be saved."); + } + + return Optional.empty(); + } + + private void handleNewProject() { + if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) { + return; + } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) { + return; + } + + final Optional result = NewProjectDialog.showDialog(view); + + if (!result.isPresent()) { + return; + } + + setProjectModel(result); + + projectModel = result; + final ProjectModel projectModel = result.get(); + + final DefaultListModel pages = view.getPages().getListModel(); + + final ThumbnailWorker thumbnailLoader = new ThumbnailWorker(projectModel, pages); + thumbnailLoader.execute(); + this.thumbnailLoader = Optional.of(thumbnailLoader); + + final PageListWorker pageListLoader = new PageListWorker(projectModel, pages); + + pageListLoader.execute(); + + setApplicationMode(ApplicationMode.PROJECT); + } + + private void setProjectModel(Optional model) { + projectModel = model; + + if (model.isPresent()) { + view.setTitle(String.format("tesseract4java - %s", model.get().getProjectName())); + } else { + view.setTitle("tesseract4java"); + + view.getPages().getListModel().removeAllElements(); + } + } + + private void handleOpenBoxFile() { + if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) { + return; + } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) { + return; + } + + final JFileChooser fc = new JFileChooser(); + + final String lastBoxFile = PreferencesUtil.getPreferences().get(KEY_BOX_FILE, null); + if (lastBoxFile != null) { + final Path dir = Paths.get(lastBoxFile).getParent(); + if (Files.isDirectory(dir)) { + fc.setCurrentDirectory(dir.toFile()); + } + } + + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Image files"; + } + + @Override + public boolean accept(File f) { + final String fname = f.getName(); + return f.canRead() + && (f.isDirectory() || f.isFile() + && (fname.endsWith(".png") + || fname.endsWith(".tif") + || fname.endsWith(".tiff") + || fname.endsWith(".jpg") + || fname.endsWith(".jpeg"))); + } + }); + final int result = fc.showOpenDialog(view); + + if (result == JFileChooser.APPROVE_OPTION) { + final Path imageFile = fc.getSelectedFile().toPath(); + + try { + final Path boxFile = FileNames.replaceExtension(imageFile, "box"); + final BufferedImage image = ImageIO.read(imageFile.toFile()); + final List boxes = BoxFileReader.readBoxFile(boxFile, image.getHeight()); + + setApplicationMode(ApplicationMode.BOX_FILE); + + view.getScale().setTo100Percent(); + + PreferencesUtil.getPreferences().put(KEY_BOX_FILE, boxFile.toAbsolutePath().toString()); + + setBoxFileModel(Optional.of(new BoxFileModel(boxFile, image, boxes))); + } catch (IOException | IndexOutOfBoundsException e) { + Dialogs.showError(view, "Error", "Box file could not be opened."); + } + } + } + + private void handleOpenProject() { + if (mode == ApplicationMode.BOX_FILE && !handleCloseBoxFile()) { + return; + } else if (mode == ApplicationMode.PROJECT && !handleCloseProject()) { + return; + } + + final JFileChooser fc = new JFileChooser(); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Tesseract Project Files (*.tesseract-project)"; + } + + @Override + public boolean accept(File f) { + return f.isFile() && f.getName().endsWith(".tesseract-project"); + } + }); + final int result = fc.showOpenDialog(view); + if (result == JFileChooser.APPROVE_OPTION) { + // TODO load project + + } + } + + private void handleSaveProject() { + // TODO fix me + final JFileChooser fc = new JFileChooser( + projectModel.get().getProjectDir().toFile()); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Tesseract Project Files (*.tesseract-project)"; + } + + @Override + public boolean accept(File f) { + return f.isFile() && f.getName().endsWith(".tesseract-project"); + } + }); + final int result = fc.showSaveDialog(view); + if (result == JFileChooser.APPROVE_OPTION) { + // TODO save project + + } + } + + private void handleSaveBoxFile() { + final Optional boxFileModel = getBoxFileModel(); + + if (boxFileModel.isPresent()) { + try { + BoxFileWriter.writeBoxFile(boxFileModel.get()); + + Dialogs.showInfo(view, "Saved", "The box file has been saved."); + } catch (IOException e) { + Dialogs.showError(view, "Error", "Box file could not be written."); + } + } else { + Dialogs.showWarning(view, "Warning", "No box file present."); + } + } + + private Optional getBoxFileModel() { + if (mode == ApplicationMode.NONE) { + return Optional.empty(); + } else if (mode == ApplicationMode.BOX_FILE) { + // first check box editor, then symbol overview + final Optional model = view.getBoxEditor().getBoxFileModel(); + + if (model.isPresent()) { + return model; + } else { + return view.getSymbolOverview().getBoxFileModel(); + } + } else { + final MainComponent active = view.getActiveComponent(); + + if (active instanceof PageModelComponent) { + return ((PageModelComponent) active).getBoxFileModel(); + } else { + return Optional.empty(); + } + } + } + + private boolean handleCloseProject() { + final boolean really = Dialogs.ask(view, "Confirmation", "Do you really want to close this project?"); + + if (really) { + setPageModel(Optional.empty()); + setProjectModel(Optional.empty()); + setApplicationMode(ApplicationMode.NONE); + } + + return really; + } + + private boolean handleCloseBoxFile() { + final boolean really = Dialogs.ask(view, "Confirmation", + "Do you really want to close this box file? All unsaved changes will be lost."); + + if (really) { + setBoxFileModel(Optional.empty()); + + setApplicationMode(ApplicationMode.NONE); + } + + return really; + } + + private void handlePageSelection() { + final PageThumbnail pt = view.getPages().getList().getSelectedValue(); + + // don't do anything, if no page is selected + if (pt == null) { + return; + } + + final Preprocessor preprocessor = getPreprocessor(pt.getFile()); + view.getPreprocessingPane().setPreprocessor(preprocessor); + + // ask to save box file + if (view.getActiveComponent() == view.getBoxEditor() && view.getBoxEditor().hasChanged()) { + final boolean changePage = Dialogs.ask( + view, + "Unsaved Changes", + "The current box file has not been saved. Do you really want to change the page?"); + + if (!changePage) { + // reselect the old page + view.getPages().getList().setSelectedValue(pageThumbnail.get(), true); + // don't change the page + return; + } + } else if (view.getActiveComponent() == view.getSymbolOverview()) { + view.getSymbolOverview().freeResources(); + } + + pageThumbnail = Optional.of(pt); + + // cancel the last page loading task if it is present + if (lastPageSelectionTask.isPresent()) { + lastPageSelectionTask.get().cancel(); + } + + // new task + final TimerTask task = new TimerTask() { + @Override + public void run() { + // cancel last task + if (preprocessingWorker.isPresent()) { + preprocessingWorker.get().cancel(false); + } + + // create SwingWorker to preprocess page + final PreprocessingWorker pw = new PreprocessingWorker( + TesseractController.this, + getPreprocessor(pt.getFile()), pt.getFile(), + getProjectModel().get().getPreprocessedDir()); + + // save reference + preprocessingWorker = Optional.of(pw); + + view.getProgressBar().setIndeterminate(true); + // execute it + pw.execute(); + } + }; + + // run the page loader with a delay of 1 second + // the user has 1 second to change the page before it starts loading + pageSelectionTimer.schedule(task, 500); + + // set as new timer task + lastPageSelectionTask = Optional.of(task); + } + + private void handleShowSymbolInBoxEditor() { + final Symbol selected = view.getSymbolOverview().getSymbolVariantList().getList().getSelectedValue(); + + if (selected == null) { + return; + } + + view.getMainTabs().setSelectedComponent(view.getBoxEditor()); + + final FilteredTable symbols = view.getBoxEditor().getSymbols(); + symbols.getTextField().setText(""); + final ListModel model = symbols.getListModel(); + final int size = model.getSize(); + + // find the selected symbol in + for (int i = 0; i < size; i++) { + if (selected == model.getElementAt(i)) { + symbols.getTable().setRowSelectionInterval(i, i); + } + } + } + + private void handleSymbolGroupSelection() { + final JList>> selectionList = + view.getSymbolOverview().getSymbolGroupList().getList(); + + final int index = selectionList.getSelectedIndex(); + if (index == -1) { + return; + } + + final List symbols = selectionList.getModel().getElementAt(index).getValue(); + + final Optional bfm = view.getSymbolOverview().getBoxFileModel(); + + if (!bfm.isPresent()) { + return; + } + + final SymbolListModel model = new SymbolListModel(bfm.get().getImage()); + symbols.forEach(model::addElement); + + // get combo box + final JComboBox ordering = view.getSymbolOverview().getSymbolVariantList().getOrderingComboBox(); + + // sort symbols + model.sortBy((SymbolOrder) ordering.getSelectedItem()); + + view.getSymbolOverview().getSymbolVariantList().getList().setModel(model); + } + + private void handleSymbolReordering() { + // get combo box + final JComboBox ordering = view.getSymbolOverview().getSymbolVariantList().getOrderingComboBox(); + + // get model + final SymbolListModel model = (SymbolListModel) view.getSymbolOverview() + .getSymbolVariantList().getList().getModel(); + + // sort symbols + model.sortBy((SymbolOrder) ordering.getSelectedItem()); + } + + private void handleThumbnailLoading() { + if (!thumbnailLoader.isPresent()) { + return; + } + + final ThumbnailWorker thumbnailLoader = this.thumbnailLoader.get(); + + tasks.forEach(Task::cancel); + tasks.clear(); + + lastThumbnailLoadTask.ifPresent(TimerTask::cancel); + + thumbnailLoadTimer.schedule(new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { + final JList list = + view.getPages().getList(); + final ListModel model = list.getModel(); + + final int first = list.getFirstVisibleIndex(); + final int last = list.getLastVisibleIndex(); + + for (int i = first; i <= last; i++) { + final PageThumbnail pt = model.getElementAt(i); + + if (pt == null || pt.getThumbnail().isPresent()) { + continue; + } + + final Task t = new Task(i, pt); + tasks.add(t); + thumbnailLoader.submitTask(t); + } + }); + } + }, 500); // 500ms delay + } + + private void handleTraineddataFileSelection() { + + final String traineddataFile = view.getTraineddataFiles().getList().getSelectedValue(); + + if (traineddataFile != null) { + PreferencesUtil.getPreferences().put(KEY_TRAINING_FILE, traineddataFile); + + pageRecognitionProducer.setTrainingFile(traineddataFile); + + // try { + // final Optional prototypes = loadPrototypes(); + // featureDebugger.setPrototypes(prototypes); + // } catch (IOException e) { + // e.printStackTrace(); + // } + + // if the traineddata file has changed, ask to reload the page + if (!view.getPages().getList().isSelectionEmpty() + && !Objects.equals(traineddataFile, lastTraineddataFile)) { + handlePageSelection(); + } + + lastTraineddataFile = traineddataFile; + } + } + + private void handlePreprocessorPreview() { + final Optional selectedPage = getSelectedPage(); + + // if no page is selected, simply ignore it + if (!selectedPage.isPresent()) { + Dialogs.showWarning(view, "No page selection", + "No page has been selected. You need to select a page first."); + return; + } + + final Optional projectModel = getProjectModel(); + + if (!projectModel.isPresent()) { + Dialogs.showWarning(view, "No project", + "No project has been selected. You need to create a project first."); + return; + } + + final Preprocessor preprocessor = + view.getPreprocessingPane().getPreprocessor(); + + if (preprocessingWorker.isPresent()) { + preprocessingWorker.get().cancel(false); + } + + final PreprocessingWorker pw = new PreprocessingWorker(this, + preprocessor, selectedPage.get(), + projectModel.get().getProjectDir()); + + preprocessingWorker = Optional.of(pw); + + view.getProgressBar().setIndeterminate(true); + pw.execute(); + } + + private void handlePreprocessorChange(boolean allPages) { + final Preprocessor preprocessor = view.getPreprocessingPane().getPreprocessor(); + + if (allPages + && Dialogs.ask(view, "Confirmation", + "Do you really want to apply the current preprocessing methods to all pages?")) { + defaultPreprocessor = preprocessor; + preprocessors.clear(); + + if (getSelectedPage().isPresent()) { + handlePreprocessorPreview(); + } + } else if (getSelectedPage().isPresent()) { + setPreprocessor(getSelectedPage().get(), preprocessor); + + handlePreprocessorPreview(); + } + } + + private void handlePreferences() { + final PreferencesDialog prefDialog = new PreferencesDialog(); + final ResultState result = prefDialog.showPreferencesDialog(view); + if (result == ResultState.APPROVE) { + final Preferences globalPrefs = PreferencesUtil.getPreferences(); + try { + final Path langdataDir = Paths.get(prefDialog.getTfLangdataDir().getText()); + if (Files.isDirectory(langdataDir)) { + globalPrefs.put(PreferencesDialog.KEY_LANGDATA_DIR, langdataDir.toString()); + } + + final String renderingFont = (String) prefDialog.getComboRenderingFont().getSelectedItem(); + globalPrefs.put(PreferencesDialog.KEY_RENDERING_FONT, renderingFont); + + final String editorFont = (String) prefDialog.getComboEditorFont().getSelectedItem(); + globalPrefs.put(PreferencesDialog.KEY_EDITOR_FONT, editorFont); + + // Update the page segmentation mode if necessary + int currentPageSegMode = getPageSegmentationMode(); + int pageSegMode = prefDialog.getPageSegmentationMode(); + boolean hasPageSegModeChanged = currentPageSegMode != pageSegMode; + if (hasPageSegModeChanged) { + globalPrefs.putInt(PreferencesDialog.KEY_PAGE_SEG_MODE, pageSegMode); + pageRecognitionProducer.setPageSegmentationMode(pageSegMode); + + // Update model with new segmentation mode + if (activeComponent instanceof PageModelComponent) { + final Optional pm = ((PageModelComponent) activeComponent).getPageModel(); + pm.ifPresent(it -> setImageModel(Optional.of(it.getImageModel()))); + } + } + + view.getRecognitionPane().setRenderingFont(renderingFont); + if (view.getActiveComponent() == view.getRecognitionPane()) { + view.getRecognitionPane().render(); + } + + view.getEvaluationPane().setEditorFont(editorFont); + } catch (Exception e) { + Dialogs.showWarning(view, "Error", "Could not save the preferences."); + } + } + } + + private void handleCharacterHistogram() { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Text files"; + } + + @Override + public boolean accept(File f) { + return f.canRead(); + } + }); + + final int approved = fc.showOpenDialog(view); + if (approved == JFileChooser.APPROVE_OPTION) { + final Path textFile = fc.getSelectedFile().toPath(); + + try { + final BufferedReader reader = Files.newBufferedReader(textFile, StandardCharsets.UTF_8); + + final Map histogram = new TreeMap<>(); + + // build up a histogram + int c; + while ((c = reader.read()) != -1) { + final char character = (char) c; + + Integer val = histogram.get(character); + + if (val == null) { + val = 0; + } + + histogram.put(character, val + 1); + } + + final CharacterHistogram ch = new CharacterHistogram(histogram); + ch.setLocationRelativeTo(view); + ch.setVisible(true); + + } catch (IOException e) { + Dialogs.showError(view, "Invalid text file", "Could not read the text file."); + } + } + } + + private void handleInspectUnicharset() { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Unicharset files"; + } + + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().endsWith("unicharset"); + } + }); + + final int approved = fc.showOpenDialog(view); + if (approved == JFileChooser.APPROVE_OPTION) { + final Path unicharsetFile = fc.getSelectedFile().toPath(); + try (final BufferedReader unicharsetReader = + Files.newBufferedReader(unicharsetFile, StandardCharsets.UTF_8)) { + + final Unicharset unicharset = Unicharset.readFrom(unicharsetReader); + + // show the unicharset dialog + final UnicharsetDebugger uniDebugger = new UnicharsetDebugger(unicharset); + uniDebugger.setLocationRelativeTo(view); + uniDebugger.setVisible(true); + } catch (IOException e) { + Dialogs.showError(view, "Invalid Unicharset", + "Could not read the unicharset file. It may have an incompatible version."); + } + } + } + + private void handleTesseractTrainer() { + final TesseractTrainer trainer = new TesseractTrainer(); + trainer.setLocationRelativeTo(view); + trainer.setVisible(true); + } + + private int getPageSegmentationMode() { + return PreferencesUtil.getPreferences().getInt(PreferencesDialog.KEY_PAGE_SEG_MODE, PreferencesDialog.DEFAULT_PSM_MODE); + } + + public void setPageModel(Optional model) { + if (projectModel.isPresent() && model.isPresent()) { + try { + // plain text file name + final Path filename = + FileNames.replaceExtension(model.get().getImageModel().getSourceFile().getFileName(), "txt"); + + // create ocr directory + Files.createDirectories(projectModel.get().getOCRDir()); + + // write the plain text ocr file + final Path plain = projectModel.get().getOCRDir().resolve(filename); + + final Writer writer = Files.newBufferedWriter(plain, StandardCharsets.UTF_8); + new PlainTextWriter(true).write(model.get().getPage(), writer); + writer.close(); + + // read the transcription file + final Path transcriptionFile = projectModel.get().getTranscriptionDir().resolve(filename); + + if (Files.isRegularFile(transcriptionFile)) { + final byte[] bytes = Files.readAllBytes(transcriptionFile); + final String transcription = new String(bytes, StandardCharsets.UTF_8); + + model = Optional.of(model.get().withTranscription(transcription)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + final MainComponent active = view.getActiveComponent(); + + if (active instanceof PageModelComponent) { + ((PageModelComponent) active).setPageModel(model); + } else if (active instanceof BoxFileModelComponent) { + if (model.isPresent()) { + ((BoxFileModelComponent) active).setBoxFileModel(Optional.of(model.get().toBoxFileModel())); + } else { + ((BoxFileModelComponent) active).setBoxFileModel(Optional.empty()); + } + } + } + + public void setBoxFileModel(Optional model) { + final MainComponent active = view.getActiveComponent(); + if (active instanceof BoxFileModelComponent) { + ((BoxFileModelComponent) active).setBoxFileModel(model); + } else { + Dialogs.showWarning(view, "Illegal Action", "Could not set the box file"); + } + } + + public void setImageModel(Optional model) { + view.getProgressBar().setIndeterminate(false); + final MainComponent active = view.getActiveComponent(); + + if (active instanceof PageModelComponent) { + ((PageModelComponent) active).setPageModel(Optional.empty()); + + if (recognitionWorker.isPresent()) { + recognitionWorker.get().cancel(false); + } + + final Optional trainingFile = getTrainingFile(); + + if (!trainingFile.isPresent()) { + Dialogs.showWarning(view, "Warning", "Please select a traineddata file."); + return; + } else if (!model.isPresent()) { + return; + } + + final RecognitionWorker rw = new RecognitionWorker(this, model.get(), trainingFile.get()); + + rw.execute(); + + recognitionWorker = Optional.of(rw); + + return; + } else if (!(active instanceof ImageModelComponent)) { + return; + } + + if (!model.isPresent()) { + ((ImageModelComponent) active).setImageModel(model); + return; + } + + final Path sourceFile = model.get().getSourceFile(); + final Optional selectedPage = getSelectedPage(); + + if (!selectedPage.isPresent() || !sourceFile.equals(selectedPage.get())) { + + ((ImageModelComponent) active).setImageModel(Optional.empty()); + return; + } + + ((ImageModelComponent) active).setImageModel(model); + } + + // TODO prototype loading? + // private Optional loadPrototypes() throws IOException { + // final Path tessdir = TraineddataFiles.getTessdataDir(); + // final Path base = tmpDir.resolve(TMP_TRAINING_FILE_BASE); + // + // TessdataManager.extract( + // tessdir.resolve(lastTraineddataFile + ".traineddata"), base); + // + // final Path prototypeFile = + // tmpDir.resolve(tmpDir.resolve(TMP_TRAINING_FILE_BASE + // + "inttemp")); + // + // final InputStream in = Files.newInputStream(prototypeFile); + // final InputBuffer buf = + // InputBuffer.allocate(new BufferedInputStream(in)); + // + // try { + // final IntTemplates prototypes = IntTemplates.readFrom(buf); + // + // return Optional.of(prototypes); + // } catch (IOException e) { + // throw e; + // } finally { + // // close input buffer, even if an error occurred + // buf.close(); + // } + // } + + @Override + public void stateChanged(ChangeEvent evt) { + final Object source = evt.getSource(); + if (source == view.getPages().getList().getParent()) { + handleThumbnailLoading(); + } else if (source == view.getMainTabs()) { + handleActiveComponentChange(); + } + } + + @Override + public void update(Observable o, Object arg) { + if (o == view.getScale()) { + view.getScaleLabel().setText(o.toString()); + } + } + + @Override + public void valueChanged(ListSelectionEvent evt) { + if (evt.getValueIsAdjusting()) { + return; + } + + final Object source = evt.getSource(); + if (source.equals(view.getPages().getList())) { + handlePageSelection(); + } else if (source.equals(view.getTraineddataFiles().getList())) { + handleTraineddataFileSelection(); + } else if (source.equals(view.getSymbolOverview().getSymbolGroupList().getList())) { + handleSymbolGroupSelection(); + } + } + + private void handleExit() { + windowClosing(new WindowEvent(view, WindowEvent.WINDOW_CLOSING)); + + if (!view.isVisible()) { + windowClosed(new WindowEvent(view, WindowEvent.WINDOW_CLOSED)); + } + } + + @Override + public void windowClosing(WindowEvent e) { + if (mode == ApplicationMode.PROJECT) { + if (!handleCloseProject()) { + return; + } + } else if (mode == ApplicationMode.BOX_FILE) { + if (!handleCloseBoxFile()) { + return; + } + } + + pageSelectionTimer.cancel(); + thumbnailLoadTimer.cancel(); + + if (preprocessingWorker.isPresent()) { + preprocessingWorker.get().cancel(true); + } + + if (recognitionWorker.isPresent()) { + recognitionWorker.get().cancel(true); + } + + view.dispose(); + } + + @Override + public void windowClosed(WindowEvent evt) { + // forcefully shut down the application after 3 seconds + try { + Thread.sleep(3000); + System.exit(0); + } catch (InterruptedException e) { + System.exit(0); + } + } + + public Preprocessor getDefaultPreprocessor() { + return defaultPreprocessor; + } + + public Preprocessor getPreprocessor(Path sourceFile) { + final Preprocessor preprocessor = preprocessors.get(sourceFile); + + if (preprocessor == null) { + return defaultPreprocessor; + } + + return preprocessors.get(sourceFile); + } + + public boolean hasPreprocessorChanged(Path sourceFile) { + // try to remove it and return true if the set contained the sourceFile + return changedPreprocessors.contains(sourceFile); + } + + public void setDefaultPreprocessor(Preprocessor preprocessor) { + defaultPreprocessor = preprocessor; + } + + public void setPreprocessor(Path sourceFile, Preprocessor preprocessor) { + if (preprocessor.equals(defaultPreprocessor)) { + preprocessors.remove(sourceFile); + } else { + preprocessors.put(sourceFile, preprocessor); + } + } + + public void setPreprocessorChanged(Path sourceFile, boolean changed) { + if (changed) { + changedPreprocessors.add(sourceFile); + } else { + changedPreprocessors.remove(sourceFile); + } + } + + public void setApplicationMode(ApplicationMode mode) { + this.mode = mode; + final JTabbedPane mainTabs = view.getMainTabs(); + + final boolean projectEnabled; + final boolean boxFileEnabled; + if (mode == ApplicationMode.NONE) { + mainTabs.setEnabled(false); + projectEnabled = false; + boxFileEnabled = false; + } else { + mainTabs.setEnabled(true); + boxFileEnabled = true; + + if (mode == ApplicationMode.BOX_FILE) { + // set box file tabs active + mainTabs.setEnabledAt(0, false); + mainTabs.setEnabledAt(1, true); + mainTabs.setEnabledAt(2, true); + mainTabs.setEnabledAt(3, false); + mainTabs.setEnabledAt(4, false); + mainTabs.setSelectedIndex(1); + + projectEnabled = false; + } else { + // set all tabs active + mainTabs.setEnabledAt(0, true); + mainTabs.setEnabledAt(1, true); + mainTabs.setEnabledAt(2, true); + mainTabs.setEnabledAt(3, true); + mainTabs.setEnabledAt(4, true); + + projectEnabled = true; + } + } + + view.getMenuItemSaveBoxFile().setEnabled(boxFileEnabled); + // view.getMenuItemSavePage().setEnabled(projectEnabled); + // view.getMenuItemSaveProject().setEnabled(projectEnabled); + view.getMenuItemOpenProjectDirectory().setEnabled(projectEnabled); + view.getMenuItemBatchExport().setEnabled(projectEnabled); + view.getMenuItemImportTranscriptions().setEnabled(projectEnabled); + view.getMenuItemCloseProject().setEnabled(projectEnabled); + + view.getSymbolOverview().getSymbolVariantList().getCompareToPrototype() + .setVisible(projectEnabled); + } + + public ApplicationMode getApplicationMode() { + return mode; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java new file mode 100644 index 00000000..eeeab04b --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/TesseractTrainer.java @@ -0,0 +1,480 @@ +package de.vorb.tesseract.gui.controller; + +import de.vorb.tesseract.gui.model.PreferencesUtil; +import de.vorb.tesseract.gui.view.TesseractFrame; +import de.vorb.tesseract.gui.view.dialogs.Dialogs; +import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog; +import de.vorb.util.FileNames; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import java.awt.Cursor; +import java.awt.Desktop; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.LinkedList; +import java.util.List; + +public class TesseractTrainer extends JDialog { + private static final long serialVersionUID = 1L; + + private static final String KEY_TRAINING_DIR = "training_dir"; + + private JPanel contentPane; + private JTextField tfTrainingDir; + private JTextField tfLangdataDir; + private JTextField tfExecutablesDir; + private JCheckBox checkUseLangdata; + + public static void main(String[] args) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e1) { + // fail silently + try { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } catch (Exception e2) { + // fail silently + } + + // If the system LaF is not available, use whatever LaF is already + // being used. + } + + final TesseractTrainer trainer = new TesseractTrainer(); + trainer.setVisible(true); + } + + /** + * Create the frame. + */ + public TesseractTrainer() { + final Toolkit t = Toolkit.getDefaultToolkit(); + + // load and set multiple icon sizes + final List appIcons = new LinkedList<>(); + appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_16.png"))); + appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_96.png"))); + appIcons.add(t.getImage(TesseractFrame.class.getResource("/logos/logo_256.png"))); + setIconImages(appIcons); + + setTitle("Tesseract Trainer"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setModalityType(ModalityType.APPLICATION_MODAL); + + setBounds(100, 100, 450, 300); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(contentPane); + GridBagLayout gbl_contentPane = new GridBagLayout(); + gbl_contentPane.columnWidths = new int[]{0, 0, 0, 0}; + gbl_contentPane.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0, 0}; + gbl_contentPane.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE}; + gbl_contentPane.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Double.MIN_VALUE}; + contentPane.setLayout(gbl_contentPane); + + JLabel lblExecutablesDirectory = new JLabel("Executables Directory:"); + GridBagConstraints gbc_lblExecutablesDirectory = new GridBagConstraints(); + gbc_lblExecutablesDirectory.anchor = GridBagConstraints.EAST; + gbc_lblExecutablesDirectory.insets = new Insets(0, 0, 5, 5); + gbc_lblExecutablesDirectory.gridx = 0; + gbc_lblExecutablesDirectory.gridy = 0; + contentPane.add(lblExecutablesDirectory, gbc_lblExecutablesDirectory); + + tfExecutablesDir = new JTextField(); + GridBagConstraints gbc_tfExecutablesDir = new GridBagConstraints(); + gbc_tfExecutablesDir.insets = new Insets(0, 0, 5, 5); + gbc_tfExecutablesDir.fill = GridBagConstraints.HORIZONTAL; + gbc_tfExecutablesDir.gridx = 1; + gbc_tfExecutablesDir.gridy = 0; + contentPane.add(tfExecutablesDir, gbc_tfExecutablesDir); + tfExecutablesDir.setColumns(10); + + JButton btnSelectExecutablesDir = new JButton("Select..."); + btnSelectExecutablesDir.addActionListener(evt -> { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + fc.setCurrentDirectory(new File(tfExecutablesDir.getText())); + final int result = fc.showOpenDialog(TesseractTrainer.this); + if (result == JFileChooser.APPROVE_OPTION) { + final File dir = fc.getSelectedFile(); + tfExecutablesDir.setText(dir.getAbsolutePath()); + } + }); + GridBagConstraints gbc_btnSelectExecutablesDir = new GridBagConstraints(); + gbc_btnSelectExecutablesDir.anchor = GridBagConstraints.WEST; + gbc_btnSelectExecutablesDir.insets = new Insets(0, 0, 5, 0); + gbc_btnSelectExecutablesDir.gridx = 2; + gbc_btnSelectExecutablesDir.gridy = 0; + contentPane.add(btnSelectExecutablesDir, gbc_btnSelectExecutablesDir); + + JLabel lblTrainingDirectory = new JLabel("Training Directory:"); + GridBagConstraints gbc_lblTrainingDirectory = new GridBagConstraints(); + gbc_lblTrainingDirectory.insets = new Insets(0, 0, 5, 5); + gbc_lblTrainingDirectory.anchor = GridBagConstraints.EAST; + gbc_lblTrainingDirectory.gridx = 0; + gbc_lblTrainingDirectory.gridy = 1; + contentPane.add(lblTrainingDirectory, gbc_lblTrainingDirectory); + + tfTrainingDir = new JTextField(PreferencesUtil.getPreferences().get(KEY_TRAINING_DIR, "")); + GridBagConstraints gbc_textField = new GridBagConstraints(); + gbc_textField.insets = new Insets(0, 0, 5, 5); + gbc_textField.fill = GridBagConstraints.HORIZONTAL; + gbc_textField.gridx = 1; + gbc_textField.gridy = 1; + contentPane.add(tfTrainingDir, gbc_textField); + tfTrainingDir.setColumns(30); + + JButton btnSelectTrainingDir = new JButton("Select..."); + btnSelectTrainingDir.addActionListener(evt -> { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + fc.setCurrentDirectory(new File(tfTrainingDir.getText())); + final int result = fc.showOpenDialog(TesseractTrainer.this); + if (result == JFileChooser.APPROVE_OPTION) { + final File dir = fc.getSelectedFile(); + tfTrainingDir.setText(dir.getAbsolutePath()); + PreferencesUtil.getPreferences().put(KEY_TRAINING_DIR, dir.getAbsolutePath()); + } + }); + GridBagConstraints gbc_btnSelectTrainingDir = new GridBagConstraints(); + gbc_btnSelectTrainingDir.anchor = GridBagConstraints.WEST; + gbc_btnSelectTrainingDir.insets = new Insets(0, 0, 5, 0); + gbc_btnSelectTrainingDir.gridx = 2; + gbc_btnSelectTrainingDir.gridy = 1; + contentPane.add(btnSelectTrainingDir, gbc_btnSelectTrainingDir); + + checkUseLangdata = new JCheckBox("Set unicharset properties"); + checkUseLangdata.setToolTipText("Requires 3.03+ training tools"); + GridBagConstraints gbc_chckbxUseLangdata = new GridBagConstraints(); + gbc_chckbxUseLangdata.anchor = GridBagConstraints.WEST; + gbc_chckbxUseLangdata.insets = new Insets(0, 0, 5, 5); + gbc_chckbxUseLangdata.gridx = 1; + gbc_chckbxUseLangdata.gridy = 2; + contentPane.add(checkUseLangdata, gbc_chckbxUseLangdata); + + JLabel lblLangdataDirectory = new JLabel("Langdata Directory:"); + GridBagConstraints gbc_lblLangdataDirectory = new GridBagConstraints(); + gbc_lblLangdataDirectory.anchor = GridBagConstraints.EAST; + gbc_lblLangdataDirectory.insets = new Insets(0, 0, 5, 5); + gbc_lblLangdataDirectory.gridx = 0; + gbc_lblLangdataDirectory.gridy = 3; + contentPane.add(lblLangdataDirectory, gbc_lblLangdataDirectory); + + tfLangdataDir = new JTextField(PreferencesUtil.getPreferences().get(PreferencesDialog.KEY_LANGDATA_DIR, "")); + tfLangdataDir.setEnabled(false); + GridBagConstraints gbc_tfLangdataDir = new GridBagConstraints(); + gbc_tfLangdataDir.insets = new Insets(0, 0, 5, 5); + gbc_tfLangdataDir.fill = GridBagConstraints.HORIZONTAL; + gbc_tfLangdataDir.gridx = 1; + gbc_tfLangdataDir.gridy = 3; + contentPane.add(tfLangdataDir, gbc_tfLangdataDir); + tfLangdataDir.setColumns(30); + + final JButton btnSelectLangdataDir = new JButton("Select..."); + btnSelectLangdataDir.addActionListener(evt -> { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + fc.setCurrentDirectory(new File(tfLangdataDir.getText())); + final int result = fc.showOpenDialog(TesseractTrainer.this); + if (result == JFileChooser.APPROVE_OPTION) { + final File dir = fc.getSelectedFile(); + tfLangdataDir.setText(dir.getAbsolutePath()); + PreferencesUtil.getPreferences().put(PreferencesDialog.KEY_LANGDATA_DIR, dir.getAbsolutePath()); + } + }); + btnSelectLangdataDir.setEnabled(false); + GridBagConstraints gbc_btnSelectLangdataDir = new GridBagConstraints(); + gbc_btnSelectLangdataDir.anchor = GridBagConstraints.WEST; + gbc_btnSelectLangdataDir.insets = new Insets(0, 0, 5, 0); + gbc_btnSelectLangdataDir.gridx = 2; + gbc_btnSelectLangdataDir.gridy = 3; + + contentPane.add(btnSelectLangdataDir, gbc_btnSelectLangdataDir); + checkUseLangdata.addActionListener(evt -> { + tfLangdataDir.setEnabled(checkUseLangdata.isSelected()); + btnSelectLangdataDir.setEnabled(checkUseLangdata.isSelected()); + }); + + JButton btnTrain = new JButton("Train"); + btnTrain.setIcon(new ImageIcon( + TesseractTrainer.class.getResource("/icons/wand.png"))); + + GridBagConstraints gbc_btnTrain = new GridBagConstraints(); + gbc_btnTrain.fill = GridBagConstraints.HORIZONTAL; + gbc_btnTrain.insets = new Insets(0, 0, 0, 5); + gbc_btnTrain.gridx = 1; + gbc_btnTrain.gridy = 6; + contentPane.add(btnTrain, gbc_btnTrain); + + pack(); + setMinimumSize(getSize()); + + btnTrain.addActionListener(new Trainer()); + } + + private class Trainer implements ActionListener { + public void actionPerformed(ActionEvent evt) { + final Path execDir = Paths.get(tfExecutablesDir.getText()); + if (!Files.isDirectory(execDir)) { + Dialogs.showError(TesseractTrainer.this, "Error", "Invalid executables directory."); + return; + } + final String cmdDir = execDir + File.separator; + + final Path trainingDir = Paths.get(tfTrainingDir.getText()); + if (!Files.isDirectory(trainingDir) + || !Files.isWritable(trainingDir)) { + Dialogs.showError(TesseractTrainer.this, "Error", "Invalid training directory."); + return; + } + + final Path langdataDir = Paths.get(tfLangdataDir.getText()); + if (checkUseLangdata.isSelected() + && !Files.isDirectory(langdataDir)) { + Dialogs.showError(TesseractTrainer.this, "Error", "Invalid langdata directory."); + return; + } + + // indeterminate + TesseractTrainer.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + Files.deleteIfExists(trainingDir.resolve("training.log")); + + // create log stream + + try (final PrintStream log = new PrintStream(Files.newOutputStream( + trainingDir.resolve("training.log")), true, "UTF-8")) { + + final DirectoryStream ds = Files.newDirectoryStream(trainingDir, new TrainingFileFilter()); + + ProcessBuilder pb; + InputStream err; + int c; + + final LinkedList boxFiles = new LinkedList<>(); + final LinkedList trFiles = new LinkedList<>(); + + String base = null; + + // train + for (Path file : ds) { + final String sample = file.toString(); + final String sampleBase = sample.replaceFirst("\\.[^.]+$", ""); + + if (base == null) { + final String fname = file.getFileName().toString(); + base = file.getParent() + .resolve(fname.replaceFirst("\\..+", "") + ".") + .toString(); + } + + boxFiles.add(sampleBase + ".box"); + trFiles.add(sampleBase + ".tr"); + + pb = new ProcessBuilder(cmdDir + "tesseract", sample, sampleBase, "box.train") + .directory(trainingDir.toFile()); + + log.println("tesseract " + sample + " box.train:\n"); + final Process train = pb.start(); + err = train.getErrorStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + log.println(); + + if (train.waitFor() != 0) { + throw new Exception("Unable to train '" + sample + "'."); + } + } + + final String lang = Paths.get(base).getFileName().toString(); + + // delete old unicharset + Files.deleteIfExists(trainingDir.resolve("unicharset")); + + // extract unicharset + final List uniExtractor = new LinkedList<>(); + uniExtractor.add(cmdDir + "unicharset_extractor"); + uniExtractor.addAll(boxFiles); + + pb = new ProcessBuilder(uniExtractor).directory(trainingDir.toFile()); + + log.println("\nunicharset_extractor:\n"); + final Process unicharset = pb.start(); + err = unicharset.getInputStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + if (unicharset.waitFor() != 0) { + throw new Exception("Unable to extract unicharset."); + } + + // set unicharset properties + if (checkUseLangdata.isSelected()) { + pb = new ProcessBuilder(cmdDir + "set_unicharset_properties", + "-U", "unicharset", "-O", "out.unicharset", + "--script_dir=" + langdataDir).directory( + trainingDir.toFile()); + + log.println("\nset_unicharset_properties:\n"); + final Process uniProps = pb.start(); + err = uniProps.getErrorStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + if (uniProps.waitFor() != 0) { + throw new Exception("Unable to set unicharset properties."); + } + } else { + Files.copy(trainingDir.resolve("unicharset"), + trainingDir.resolve("out.unicharset"), + StandardCopyOption.REPLACE_EXISTING); + } + + // mftraining + final List mfTraining = new LinkedList<>(); + mfTraining.add(cmdDir + "mftraining"); + mfTraining.add("-F"); + mfTraining.add(lang + "font_properties"); + mfTraining.add("-U"); + mfTraining.add("out.unicharset"); + mfTraining.addAll(trFiles); + pb = new ProcessBuilder(mfTraining).directory(trainingDir.toFile()); + + log.println("\nmftraining:\n"); + final Process mfTrain = pb.start(); + err = mfTrain.getErrorStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + if (mfTrain.waitFor() != 0) { + throw new Exception("Unable to do mftraining."); + } + + // cntraining + final List cnTrainingParams = new LinkedList<>(); + cnTrainingParams.add(cmdDir + "cntraining"); + cnTrainingParams.addAll(trFiles); + + pb = new ProcessBuilder(cnTrainingParams).directory(trainingDir.toFile()); + + log.println("\ncntraining:\n"); + final Process cnTraining = pb.start(); + err = cnTraining.getErrorStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + if (cnTraining.waitFor() != 0) { + throw new Exception("Unable to do cntraining."); + } + + // rename files + Files.move(trainingDir.resolve("inttemp"), + trainingDir.resolve(lang + "inttemp"), + StandardCopyOption.REPLACE_EXISTING); + Files.move(trainingDir.resolve("normproto"), + trainingDir.resolve(lang + "normproto"), + StandardCopyOption.REPLACE_EXISTING); + Files.move(trainingDir.resolve("out.unicharset"), + trainingDir.resolve(lang + "unicharset"), + StandardCopyOption.REPLACE_EXISTING); + Files.move(trainingDir.resolve("pffmtable"), + trainingDir.resolve(lang + "pffmtable"), + StandardCopyOption.REPLACE_EXISTING); + Files.move(trainingDir.resolve("shapetable"), + trainingDir.resolve(lang + "shapetable"), + StandardCopyOption.REPLACE_EXISTING); + + // combine + pb = new ProcessBuilder(cmdDir + "combine_tessdata", lang).directory(trainingDir.toFile()); + + log.println("\ncombine_tessdata:\n"); + final Process combine = pb.start(); + err = combine.getErrorStream(); + + while ((c = err.read()) != -1) { + log.print((char) c); + } + + if (combine.waitFor() != 0) { + throw new Exception("Unable to combine the traineddata files."); + } + + Dialogs.showInfo(TesseractTrainer.this, + "Training Complete", + "Training completed successfully."); + } catch (Exception e) { + Dialogs.showError(TesseractTrainer.this, "Error", + "Training failed. " + e.getMessage()); + } finally { + TesseractTrainer.this.setCursor(Cursor.getDefaultCursor()); + + try { + Desktop.getDesktop().open( + trainingDir.resolve("training.log").toFile()); + } catch (IOException e) { + Dialogs.showWarning(TesseractTrainer.this, "Warning", + "Could not open training log file."); + } + } + } catch (IOException e) { + Dialogs.showError(TesseractTrainer.this, "Error", + "Training failed. " + e.getMessage()); + } + } + } + + private static class TrainingFileFilter implements + DirectoryStream.Filter { + @Override + public boolean accept(Path entry) + throws IOException { + final String filename = entry.getFileName().toString(); + final boolean isImage = filename.endsWith(".png") + || filename.endsWith(".tif") + || filename.endsWith(".tiff") + || filename.endsWith(".jpg") + || filename.endsWith(".jpeg"); + if (!isImage) { + return false; + } + + final Path boxFile = FileNames.replaceExtension(entry, "box"); + return Files.isRegularFile(boxFile); + } + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/controller/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java similarity index 61% rename from src/main/java/de/vorb/tesseract/gui/controller/package-info.java rename to gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java index 8a1fb9c7..69889f5a 100644 --- a/src/main/java/de/vorb/tesseract/gui/controller/package-info.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/controller/package-info.java @@ -1,6 +1,6 @@ /** * Controller classes for TesseractGUI. - * + * * @author Paul Vorbach */ -package de.vorb.tesseract.gui.controller; \ No newline at end of file +package de.vorb.tesseract.gui.controller; diff --git a/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java similarity index 72% rename from src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java rename to gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java index ff7ff2e5..e37e4f94 100644 --- a/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/ComparatorSettingsChangeListener.java @@ -1,5 +1,5 @@ package de.vorb.tesseract.gui.event; public interface ComparatorSettingsChangeListener { - public void settingsChanged(); + void settingsChanged(); } diff --git a/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java similarity index 60% rename from src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java rename to gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java index 5fcfb960..af9c124e 100644 --- a/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/PageChangeListener.java @@ -1,5 +1,5 @@ package de.vorb.tesseract.gui.event; public interface PageChangeListener { - public void pageSelectionChanged(int pageIndex); + void pageSelectionChanged(int pageIndex); } diff --git a/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java similarity index 71% rename from src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java rename to gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java index e64a3520..1592b5b6 100644 --- a/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/ProjectChangeListener.java @@ -3,5 +3,5 @@ import java.nio.file.Path; public interface ProjectChangeListener { - public void projectChanged(Path scanDir); + void projectChanged(Path scanDir); } diff --git a/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java similarity index 63% rename from src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java rename to gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java index b2cac2a3..198495cd 100644 --- a/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/SelectionListener.java @@ -1,5 +1,5 @@ package de.vorb.tesseract.gui.event; public interface SelectionListener { - public void selectionChanged(int index); + void selectionChanged(int index); } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java b/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java new file mode 100644 index 00000000..d9160257 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/SymbolLinkListener.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.gui.event; + +import de.vorb.tesseract.util.Symbol; + +public interface SymbolLinkListener { + void selectedSymbol(Symbol symbol); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java new file mode 100644 index 00000000..0969d2ac --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/event/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Paul Vorbach + */ +/** + * @author Paul Vorbach + * + */ +package de.vorb.tesseract.gui.event; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java new file mode 100644 index 00000000..dacafefc --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileReader.java @@ -0,0 +1,45 @@ +package de.vorb.tesseract.gui.io; + +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; + +public final class BoxFileReader { + + private BoxFileReader() {} + + public static List readBoxFile(Path boxFile, int pageHeight) + throws IOException { + + final List boxes = new LinkedList<>(); + try (final BufferedReader boxReader = Files.newBufferedReader(boxFile, StandardCharsets.UTF_8)) { + String line; + while ((line = boxReader.readLine()) != null) { + final String[] components = line.split("\\s+"); + if (components.length < 5) { + continue; + } + + final String text = components[0]; + final int x = Integer.parseInt(components[1]); + final int y = Integer.parseInt(components[2]); + final int w = Integer.parseInt(components[3]) - x; + final int h = Integer.parseInt(components[4]) - y; + + boxes.add(new Symbol(text, + new Box(x, pageHeight - y - h, w, h), 0f)); + } + } catch (NumberFormatException e) { + throw new IOException(); + } + + return boxes; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java new file mode 100644 index 00000000..8a72ce0c --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/io/BoxFileWriter.java @@ -0,0 +1,36 @@ +package de.vorb.tesseract.gui.io; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public final class BoxFileWriter { + + private BoxFileWriter() {} + + public static void writeBoxFile(BoxFileModel model) throws IOException { + final BufferedWriter boxFileWriter = Files.newBufferedWriter( + model.getFile(), StandardCharsets.UTF_8); + + final int pageHeight = model.getImage().getHeight(); + + for (Symbol symbol : model.getBoxes()) { + + final Box boundingBox = symbol.getBoundingBox(); + final int x0 = boundingBox.getX(); + final int y0 = pageHeight - boundingBox.getY() - boundingBox.getHeight(); + final int x1 = x0 + boundingBox.getWidth(); + final int y1 = y0 + boundingBox.getHeight(); + + boxFileWriter.write(String.format("%s %d %d %d %d 0\n", + symbol.getText(), x0, y0, x1, y1)); + } + + boxFileWriter.close(); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java new file mode 100644 index 00000000..a8e804fc --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/io/PlainTextWriter.java @@ -0,0 +1,69 @@ +package de.vorb.tesseract.gui.io; + +import de.vorb.tesseract.gui.work.RecognitionWriter; +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Line; +import de.vorb.tesseract.util.Page; +import de.vorb.tesseract.util.Paragraph; +import de.vorb.tesseract.util.Word; + +import java.io.IOException; +import java.io.Writer; +import java.util.Iterator; + +public class PlainTextWriter implements RecognitionWriter { + public PlainTextWriter(boolean replaceHighLetterSpacing) { + } + + @Override + public void write(Page page, Writer writer) throws IOException { + final Iterator blockIt = page.blockIterator(); + while (blockIt.hasNext()) { + final Block block = blockIt.next(); + + for (final Paragraph para : block.getParagraphs()) { + for (final Line line : para.getLines()) { + final int wordsInLine = line.getWords().size(); + int wordIndex = 0; + + StringBuilder wordBuilder = new StringBuilder(); + + for (final Word word : line.getWords()) { + final String wordText = word.getText(); + + if (wordText.length() > 1) { + if (wordBuilder.length() > 0) { + writer.write(wordBuilder.toString()); + + wordBuilder = new StringBuilder(); + + writer.write(' '); + } + + writer.write(wordText); + } else { + wordBuilder.append(word.getText()); + } + + // prevent space after last word in the line + if (++wordIndex < wordsInLine + && wordBuilder.length() == 0) { + writer.write(' '); + } else if (wordBuilder.length() > 0) { + writer.write(wordBuilder.toString()); + + wordBuilder = new StringBuilder(); + } + + } + + writer.write("\n"); + } + + writer.write('\n'); + } + + writer.write('\n'); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java new file mode 100644 index 00000000..75bc553a --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ApplicationMode.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.gui.model; + +public enum ApplicationMode { + NONE, + PROJECT, + BOX_FILE +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java new file mode 100644 index 00000000..296a607c --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/BatchExportModel.java @@ -0,0 +1,60 @@ +package de.vorb.tesseract.gui.model; + +import java.nio.file.Path; + +public class BatchExportModel { + + private final Path destinationDir; + private final boolean exportTXT; + private final boolean exportHTML; + private final boolean exportXML; + private final boolean exportImages; + private final boolean exportReports; + private final boolean openDestination; + private final int numThreads; + + public BatchExportModel(Path destinationDir, boolean exportTXT, + boolean exportHTML, boolean exportXML, boolean exportImages, + boolean exportReports, int numThreads, boolean openDestination) { + this.destinationDir = destinationDir; + this.exportTXT = exportTXT; + this.exportHTML = exportHTML; + this.exportXML = exportXML; + this.exportImages = exportImages; + this.exportReports = exportReports; + this.numThreads = numThreads; + this.openDestination = openDestination; + } + + public Path getDestinationDir() { + return destinationDir; + } + + public boolean exportTXT() { + return exportTXT; + } + + public boolean exportHTML() { + return exportHTML; + } + + public boolean exportXML() { + return exportXML; + } + + public boolean exportImages() { + return exportImages; + } + + public boolean exportReports() { + return exportReports; + } + + public boolean openDestination() { + return openDestination; + } + + public int getNumThreads() { + return numThreads; + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java similarity index 67% rename from src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java rename to gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java index d72a64f7..f1379f70 100644 --- a/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/BoxFileModel.java @@ -1,22 +1,29 @@ package de.vorb.tesseract.gui.model; +import de.vorb.tesseract.util.Symbol; + import java.awt.image.BufferedImage; +import java.nio.file.Path; import java.util.Collections; import java.util.List; -import de.vorb.tesseract.util.Symbol; - public class BoxFileModel { - private final BufferedImage bwImage; + private final Path file; + private final BufferedImage image; private final List boxes; - public BoxFileModel(BufferedImage blackAndWhiteImage, List boxes) { - this.bwImage = blackAndWhiteImage; + public BoxFileModel(Path file, BufferedImage image, List boxes) { + this.file = file; + this.image = image; this.boxes = boxes; } - public BufferedImage getScan() { - return bwImage; + public Path getFile() { + return file; + } + + public BufferedImage getImage() { + return image; } public List getBoxes() { diff --git a/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java similarity index 77% rename from src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java rename to gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java index 5334daaf..1b376f47 100644 --- a/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/CharTableModel.java @@ -1,8 +1,7 @@ package de.vorb.tesseract.gui.model; -import java.util.ArrayList; - import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; public class CharTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; @@ -30,12 +29,12 @@ public int getRowCount() { @Override public String getColumnName(int colIndex) { switch (colIndex) { - case 0: - return "Character"; - case 1: - return "Description"; - case 2: - return "Codepoint"; + case 0: + return "Character"; + case 1: + return "Description"; + case 2: + return "Code point"; } return ""; @@ -44,12 +43,12 @@ public String getColumnName(int colIndex) { @Override public Object getValueAt(int rowIndex, int colIndex) { switch (colIndex) { - case 0: - return chars.get(rowIndex); - case 1: - return Character.getName(chars.get(rowIndex)); - case 2: - return hexPad(chars.get(rowIndex), 4); + case 0: + return chars.get(rowIndex); + case 1: + return Character.getName(chars.get(rowIndex)); + case 2: + return hexPad(chars.get(rowIndex), 4); } return ""; diff --git a/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java similarity index 60% rename from src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java rename to gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java index e1986d44..43cb03a7 100644 --- a/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ComparatorMode.java @@ -1,6 +1,6 @@ package de.vorb.tesseract.gui.model; public enum ComparatorMode { - COMPARE_SCAN_HOCR, - COMPARE_HOCR_HOCR; + COMPARE_SCAN_HOCR, + COMPARE_HOCR_HOCR } diff --git a/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java similarity index 89% rename from src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java rename to gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java index 134a2d6f..a86d247d 100644 --- a/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredListModel.java @@ -1,26 +1,22 @@ package de.vorb.tesseract.gui.model; -import java.util.ArrayList; +import de.vorb.tesseract.gui.util.Filter; import javax.swing.AbstractListModel; import javax.swing.ListModel; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; - -import com.google.common.base.Optional; +import java.util.ArrayList; +import java.util.Optional; public class FilteredListModel extends AbstractListModel { private static final long serialVersionUID = 1L; - public static interface Filter { - boolean accept(T item); - } - private final ListModel source; private final ArrayList filtered = new ArrayList<>(); // if filter is absent, all items are shown - private Optional> filter = Optional.absent(); + private Optional> filter = Optional.empty(); public FilteredListModel(ListModel source) { if (source == null) { @@ -74,6 +70,10 @@ public void setFilter(Optional> filter) { @Override public T getElementAt(int index) { + if (index < 0) { + return null; + } + if (!filter.isPresent()) { return source.getElementAt(index); } @@ -89,4 +89,8 @@ public int getSize() { return filtered.size(); } + + public ListModel getSource() { + return source; + } } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java new file mode 100644 index 00000000..90fbf1e0 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/FilteredTableModel.java @@ -0,0 +1,41 @@ +package de.vorb.tesseract.gui.model; + +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.table.AbstractTableModel; + +public abstract class FilteredTableModel extends AbstractTableModel { + private static final long serialVersionUID = 1L; + + private final FilteredListModel source; + + protected FilteredTableModel(FilteredListModel source) { + this.source = source; + + source.addListDataListener(new ListDataListener() { + @Override + public void intervalRemoved(ListDataEvent evt) { + fireTableStructureChanged(); + } + + @Override + public void intervalAdded(ListDataEvent evt) { + fireTableStructureChanged(); + } + + @Override + public void contentsChanged(ListDataEvent evt) { + fireTableStructureChanged(); + } + }); + } + + public FilteredListModel getSource() { + return source; + } + + @Override + public int getRowCount() { + return source.getSize(); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java new file mode 100644 index 00000000..5d3cbbce --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ImageModel.java @@ -0,0 +1,36 @@ +package de.vorb.tesseract.gui.model; + +import java.awt.image.BufferedImage; +import java.nio.file.Path; + +public class ImageModel { + private final Path sourceFile; + private final BufferedImage sourceImage; + + private final Path preprocessedFile; + private final BufferedImage preprocessedImage; + + public ImageModel(Path sourceFile, BufferedImage sourceImage, + Path preprocessedFile, BufferedImage preprocessedImage) { + this.sourceFile = sourceFile; + this.sourceImage = sourceImage; + this.preprocessedFile = preprocessedFile; + this.preprocessedImage = preprocessedImage; + } + + public Path getSourceFile() { + return sourceFile; + } + + public BufferedImage getSourceImage() { + return sourceImage; + } + + public Path getPreprocessedFile() { + return preprocessedFile; + } + + public BufferedImage getPreprocessedImage() { + return preprocessedImage; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java new file mode 100644 index 00000000..80ca9037 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PageModel.java @@ -0,0 +1,55 @@ +package de.vorb.tesseract.gui.model; + +import de.vorb.tesseract.util.Page; +import de.vorb.tesseract.util.Symbol; +import de.vorb.util.FileNames; + +import java.awt.image.BufferedImage; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.LinkedList; + +public class PageModel { + private final ImageModel imageModel; + private final Page page; + private final String transcription; + + public PageModel(ImageModel imageModel, Page page, String string) { + this.imageModel = imageModel; + this.page = page; + this.transcription = string; + } + + public Page getPage() { + return page; + } + + public ImageModel getImageModel() { + return imageModel; + } + + public String getTranscription() { + return transcription; + } + + public PageModel withTranscription(String transcription) { + if (transcription.equals(this.transcription)) + return this; + + return new PageModel(imageModel, page, transcription); + } + + public BoxFileModel toBoxFileModel() { + final Path boxFile = FileNames.replaceExtension( + imageModel.getPreprocessedFile(), "box"); + final BufferedImage image = imageModel.getPreprocessedImage(); + + final LinkedList boxes = new LinkedList<>(); + final Iterator symbolIt = page.symbolIterator(); + while (symbolIt.hasNext()) { + boxes.add(symbolIt.next()); + } + + return new BoxFileModel(boxFile, image, boxes); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java new file mode 100644 index 00000000..ed457822 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PageThumbnail.java @@ -0,0 +1,60 @@ +package de.vorb.tesseract.gui.model; + +import java.awt.image.BufferedImage; +import java.nio.file.Path; +import java.util.Optional; + +public class PageThumbnail { + private final Path file; + private Optional thumbnail; + + public PageThumbnail(Path file, Optional thumbnail) { + this.file = file; + this.thumbnail = thumbnail; + } + + public Path getFile() { + return file; + } + + public Optional getThumbnail() { + return thumbnail; + } + + public void setThumbnail(Optional thumbnail) { + this.thumbnail = thumbnail; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((file == null) ? 0 : file.hashCode()); + result = prime * result + + ((thumbnail == null) ? 0 : thumbnail.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof PageThumbnail)) + return false; + PageThumbnail other = (PageThumbnail) obj; + if (file == null) { + if (other.file != null) + return false; + } else if (!file.equals(other.file)) + return false; + if (thumbnail == null) { + if (other.thumbnail != null) + return false; + } else if (!thumbnail.equals(other.thumbnail)) + return false; + return true; + } + +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java b/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java new file mode 100644 index 00000000..aa3e17a3 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/PreferencesUtil.java @@ -0,0 +1,13 @@ +package de.vorb.tesseract.gui.model; + +import java.util.prefs.Preferences; + +public final class PreferencesUtil { + + private PreferencesUtil() { + } + + public static Preferences getPreferences() { + return Preferences.userNodeForPackage(PreferencesUtil.class); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java new file mode 100644 index 00000000..b0d6e4f8 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/ProjectModel.java @@ -0,0 +1,93 @@ +package de.vorb.tesseract.gui.model; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ProjectModel { + public static final String PROJECT_DIR = "tesseract-project"; + public static final String THUMBNAIL_DIR = "tesseract-project/thumbs"; + public static final String PREPROCESSED_DIR = "tesseract-project/preprocessed"; + public static final String TRANSCRIPTION_DIR = "tesseract-project/transcriptions"; + public static final String OCR_DIR = "tesseract-project/ocr"; + public static final String EVALUATION_DIR = "tesseract-project/evaluation"; + + private final String projectName; + + private final Path imageDir; + + private final boolean tiffFiles; + private final boolean pngFiles; + private final boolean jpegFiles; + + private final DirectoryStream.Filter filter = entry -> { + final String e = entry.getFileName().toString(); + + if (png() && e.endsWith(".png")) { + return true; + } else if (tiff() && (e.endsWith(".tif") || e.endsWith(".tiff"))) { + return true; + } else { + return jpeg() && (e.endsWith(".jpg") || e.endsWith(".jpeg")); + } + }; + + public ProjectModel(Path projectDir, boolean tiffFiles, boolean pngFiles, boolean jpegFiles) { + projectName = projectDir.getFileName().toString(); + + this.imageDir = projectDir; + + this.tiffFiles = tiffFiles; + this.pngFiles = pngFiles; + this.jpegFiles = jpegFiles; + } + + public String getProjectName() { + return projectName; + } + + public Path getImageDir() { + return imageDir; + } + + public Path getProjectDir() { + return imageDir.resolve(PROJECT_DIR); + } + + public Path getThumbnailDir() { + return imageDir.resolve(THUMBNAIL_DIR); + } + + public Path getPreprocessedDir() { + return imageDir.resolve(PREPROCESSED_DIR); + } + + public Path getTranscriptionDir() { + return imageDir.resolve(TRANSCRIPTION_DIR); + } + + public Path getOCRDir() { + return imageDir.resolve(OCR_DIR); + } + + public Path getEvaluationDir() { + return imageDir.resolve(EVALUATION_DIR); + } + + public Iterable getImageFiles() throws IOException { + return Files.newDirectoryStream(imageDir, filter); + } + + public boolean tiff() { + return tiffFiles; + } + + public boolean png() { + return pngFiles; + } + + public boolean jpeg() { + return jpegFiles; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java b/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java new file mode 100644 index 00000000..95a1a2a2 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/Scale.java @@ -0,0 +1,100 @@ +package de.vorb.tesseract.gui.model; + +import java.util.NoSuchElementException; +import java.util.Observable; + +public class Scale extends Observable { + private static final float[] VALUES = + new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.75f, 1f, 2f}; + private int cursor = 2; + + public boolean hasNext() { + return cursor < VALUES.length - 1; + } + + public boolean hasPrevious() { + return cursor != 0; + } + + public float next() { + if (!hasNext()) + throw new NoSuchElementException(); + + cursor++; + + changed(); + + return VALUES[cursor]; + } + + public float previous() { + if (!hasPrevious()) + throw new NoSuchElementException(); + + cursor--; + + changed(); + + return VALUES[cursor]; + } + + public float current() { + return VALUES[cursor]; + } + + public void setTo100Percent() { + cursor = 6; + + changed(); + } + + private void changed() { + setChanged(); + notifyObservers(); + clearChanged(); + } + + @Override + public String toString() { + final String result; + + switch (cursor) { + case 0: + result = "10%"; + break; + case 1: + result = "20%"; + break; + case 2: + result = "30%"; + break; + case 3: + result = "40%"; + break; + case 4: + result = "50%"; + break; + case 5: + result = "75%"; + break; + case 6: + result = "100%"; + break; + case 7: + result = "200%"; + break; + default: + throw new RuntimeException("illegal scale state"); + } + + return result; + } + + public static int scaled(int coordinate, float scale) { + return Math.round(coordinate * scale); + } + + public static int unscaled(int coord, float scale) { + return Math.round(coord / scale); + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java similarity index 86% rename from src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java rename to gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java index 3e4f2b9e..62ae794d 100644 --- a/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SingleSelectionModel.java @@ -1,10 +1,10 @@ package de.vorb.tesseract.gui.model; +import de.vorb.tesseract.gui.event.SelectionListener; + import java.util.LinkedList; import java.util.List; -import de.vorb.tesseract.gui.event.SelectionListener; - public class SingleSelectionModel { private int index = -1; private List selectionListeners = new LinkedList<>(); @@ -17,9 +17,8 @@ public SingleSelectionModel() { /** * Creates a selection model with the given initial selection index. - * - * @param index - * initial selection index. + * + * @param index initial selection index. */ public SingleSelectionModel(int index) { this.index = index; @@ -27,7 +26,7 @@ public SingleSelectionModel(int index) { /** * Determines if nothing is selected. - * + * * @return true on empty selection. */ public boolean isSelectionEmpty() { @@ -43,10 +42,9 @@ public int getSelectedIndex() { /** * Sets the selected index. - * - * @param index - * Selected index. Negative values are considered as no - * selection. + * + * @param index Selected index. Negative values are considered as no + * selection. */ public void setSelectedIndex(int index) { this.index = index; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java new file mode 100644 index 00000000..fa5798d0 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolListModel.java @@ -0,0 +1,109 @@ +package de.vorb.tesseract.gui.model; + +import de.vorb.tesseract.img.BinaryImage; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.ListModel; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import java.awt.image.BufferedImage; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.Vector; + +public class SymbolListModel implements ListModel { + private final Vector data = new Vector<>(); + private final LinkedList listeners = new LinkedList<>(); + private final BufferedImage page; + + private Comparator confidenceComp = (s1, s2) -> { + final float conf1 = s1.getConfidence(); + final float conf2 = s2.getConfidence(); + + if (conf1 > conf2) + return -1; + if (conf1 < conf2) + return 1; + + return 0; + }; + + private Comparator sizeComp = (s1, s2) -> s2.getBoundingBox().getArea() + - s1.getBoundingBox().getArea(); + + private Comparator weightComp = new Comparator() { + // TODO implement cache + + @Override + public int compare(Symbol s1, Symbol s2) { + final Box bb1 = s1.getBoundingBox(); + final Box bb2 = s2.getBoundingBox(); + + final BufferedImage img1 = page.getSubimage(bb1.getX(), bb1.getY(), + bb1.getWidth(), bb1.getHeight()); + final BufferedImage img2 = page.getSubimage(bb2.getX(), bb2.getY(), + bb2.getWidth(), bb2.getHeight()); + + return BinaryImage.weight(img2) - BinaryImage.weight(img1); + } + }; + + public SymbolListModel(BufferedImage page) { + this.page = page; + } + + public void sortBy(SymbolOrder order) { + final Comparator comparator; + + switch (order) { + case CONFIDENCE: + comparator = confidenceComp; + break; + case SIZE: + comparator = sizeComp; + break; + default: + comparator = weightComp; + } + + Collections.sort(data, comparator); + + final int size = data.size(); + for (ListDataListener l : listeners) { + l.contentsChanged(new ListDataEvent(this, + ListDataEvent.CONTENTS_CHANGED, 0, size - 1)); + } + } + + @Override + public void addListDataListener(ListDataListener listener) { + listeners.add(listener); + } + + public void addElement(Symbol symbol) { + data.add(symbol); + + final int size = data.size(); + for (ListDataListener l : listeners) { + l.contentsChanged(new ListDataEvent(this, + ListDataEvent.INTERVAL_ADDED, size - 2, size - 1)); + } + } + + @Override + public Symbol getElementAt(int index) { + return data.get(index); + } + + @Override + public int getSize() { + return data.size(); + } + + @Override + public void removeListDataListener(ListDataListener listener) { + listeners.remove(listener); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java new file mode 100644 index 00000000..e461721b --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolOrder.java @@ -0,0 +1,18 @@ +package de.vorb.tesseract.gui.model; + +public enum SymbolOrder { + CONFIDENCE("Confidence"), + SIZE("Size"), + IMAGE_WEIGHT("Weight"); + + private final String name; + + SymbolOrder(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java new file mode 100644 index 00000000..f3f8e867 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java @@ -0,0 +1,86 @@ +package de.vorb.tesseract.gui.model; + +import de.vorb.tesseract.util.Symbol; + +import javax.swing.DefaultListModel; + +public class SymbolTableModel extends FilteredTableModel { + private static final long serialVersionUID = 1L; + + public SymbolTableModel() { + super(new FilteredListModel<>(new DefaultListModel<>())); + } + + @Override + public int getColumnCount() { + return 6; + } + + @Override + public Object getValueAt(int rowIndex, int colIndex) { + if (colIndex == 0) { + return rowIndex + 1; + } + + final Symbol symbol = getSource().getElementAt(rowIndex); + + switch (colIndex) { + case 1: + return symbol.getText(); + case 2: + return symbol.getBoundingBox().getX(); + case 3: + return symbol.getBoundingBox().getY(); + case 4: + return symbol.getBoundingBox().getWidth(); + case 5: + return symbol.getBoundingBox().getHeight(); + default: + throw new IndexOutOfBoundsException("undefined row or column"); + } + } + + @Override + public String getColumnName(int colIndex) { + switch (colIndex) { + case 0: + return "#"; + case 1: + return "Symbol"; + case 2: + return "X"; + case 3: + return "Y"; + case 4: + return "Width"; + case 5: + return "Height"; + default: + throw new IndexOutOfBoundsException("undefined column"); + } + } + + @Override + public Class getColumnClass(int colIndex) { + switch (colIndex) { + case 0: + return Integer.class; + case 1: + return String.class; + case 2: + return Integer.class; + case 3: + return Integer.class; + case 4: + return Integer.class; + case 5: + return Integer.class; + default: + throw new IndexOutOfBoundsException("undefined column"); + } + } + + public Symbol getSymbol(int index) { + return getSource().getElementAt(index); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java new file mode 100644 index 00000000..65111624 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/model/package-info.java @@ -0,0 +1,6 @@ +/** + * Model classes. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.gui.model; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java new file mode 100644 index 00000000..7d94ec9d --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/util/DocumentWriter.java @@ -0,0 +1,55 @@ +package de.vorb.tesseract.gui.util; + +import org.w3c.dom.Document; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class DocumentWriter { + + private DocumentWriter() {} + + private static Transformer transformer; + + static { + try { + transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, + "yes"); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + } catch (TransformerConfigurationException + | TransformerFactoryConfigurationError e) { + e.printStackTrace(); + } + } + + public static String writeToString(Document document) + throws TransformerException { + final StringWriter result = new StringWriter(); + transformer.transform(new DOMSource(document), + new StreamResult(result)); + return result.toString(); + } + + public static void writeToFile(Document document, Path file) + throws IOException, TransformerException { + final BufferedWriter writer = Files.newBufferedWriter(file, + StandardCharsets.UTF_8); + transformer.transform(new DOMSource(document), + new StreamResult(writer)); + writer.close(); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java b/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java new file mode 100644 index 00000000..4481ca80 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/util/Filter.java @@ -0,0 +1,5 @@ +package de.vorb.tesseract.gui.util; + +public interface Filter { + boolean accept(T item); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java b/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java new file mode 100644 index 00000000..353a1b69 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/util/FilterProvider.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.gui.util; + +import java.util.Optional; + +public interface FilterProvider { + Optional> getFilterFor(String filterText); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java b/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java new file mode 100644 index 00000000..2182f0b0 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/util/Resources.java @@ -0,0 +1,13 @@ +package de.vorb.tesseract.gui.util; + +import javax.swing.Icon; +import javax.swing.ImageIcon; + +public final class Resources { + + private Resources() {} + + public static Icon getIcon(String name) { + return new ImageIcon(Resources.class.getResource(String.format("/icons/%s.png", name))); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java new file mode 100644 index 00000000..0fbb340a --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java @@ -0,0 +1,637 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.model.Scale; +import de.vorb.tesseract.gui.model.SingleSelectionModel; +import de.vorb.tesseract.gui.model.SymbolTableModel; +import de.vorb.tesseract.gui.util.Filter; +import de.vorb.tesseract.gui.view.renderer.BoxFileRenderer; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Point; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.DefaultListModel; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TableModelEvent; +import javax.swing.table.TableColumnModel; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import static de.vorb.tesseract.gui.model.Scale.scaled; +import static de.vorb.tesseract.gui.model.Scale.unscaled; + +public class BoxEditor extends JPanel implements BoxFileModelComponent { + private static final long serialVersionUID = 1L; + + private static final Dimension DEFAULT_SPINNER_DIMENSION = + new Dimension(50, 20); + + private final BoxFileRenderer renderer; + + // state + private final Scale scale; + private boolean changed = false; + + private Optional model = Optional.empty(); + private Optional pageModel = Optional.empty(); + + private final SingleSelectionModel selectionModel = + new SingleSelectionModel(); + + private final FilteredTable tabSymbols; + private final JLabel lblCanvas; + private final JPopupMenu contextMenu; + private final JTextField tfSymbol; + private final JSpinner spinnerX; + private final JSpinner spinnerY; + private final JSpinner spinnerWidth; + private final JSpinner spinnerHeight; + + // events + private final List changeListeners = new ArrayList<>(); + + private final PropertyChangeListener spinnerListener = + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!evt.getPropertyName().startsWith("SPIN")) { + return; + } + + // don't do anything if no symbol is selected + final Optional currentSymbol = getSelectedSymbol(); + if (!currentSymbol.isPresent()) { + return; + } + + final Object source = evt.getSource(); + + // if the source is one of the JSpinners for x, y, width and + // height, update the bounding box + if (source instanceof JSpinner) { + // get coordinates + final int x = (int) spinnerX.getValue(); + final int y = (int) spinnerY.getValue(); + final int width = (int) spinnerWidth.getValue(); + final int height = (int) spinnerHeight.getValue(); + + // update bounding box + final Box boundingBox = currentSymbol.get().getBoundingBox(); + boundingBox.setX(x); + boundingBox.setY(y); + boundingBox.setWidth(width); + boundingBox.setHeight(height); + + // re-render the whole model + renderer.render(getBoxFileModel(), scale.current()); + } + + // propagate table change + final JTable table = tabSymbols.getTable(); + table.tableChanged(new TableModelEvent(table.getModel(), + table.getSelectedRow())); + + changed = true; + } + }; + + /** + * Create the panel. + */ + public BoxEditor(final Scale scale) { + setLayout(new BorderLayout(0, 0)); + + renderer = new BoxFileRenderer(this); + + this.scale = scale; + + // create table first, so it can be used by the property change listener + tabSymbols = new FilteredTable<>(new SymbolTableModel(), + filterText -> { + final Filter filter; + + if (filterText.isEmpty()) { + filter = null; + } else { + // split filter text into terms + final String[] terms = + filterText.toLowerCase().split("\\s+"); + + filter = item -> { + // accept if at least one term is contained + final String symbolText = + item.getText().toLowerCase(); + + for (String term : terms) { + if (symbolText.contains(term)) { + return true; + } + } + return false; + }; + } + + return Optional.ofNullable(filter); + }); + + tabSymbols.getListModel().addListDataListener(new ListDataListener() { + private long last = 0L; + + @Override + public void intervalRemoved(ListDataEvent evt) { + update(); + } + + @Override + public void intervalAdded(ListDataEvent evt) { + update(); + } + + @Override + public void contentsChanged(ListDataEvent evt) { + update(); + } + + private void update() { + long now = System.currentTimeMillis(); + if (now - last > 1000) { + renderer.render(model, scale.current()); + } + last = now; + } + }); + + final JTable table = tabSymbols.getTable(); + table.setFillsViewportHeight(true); + + { + // set column widths + final TableColumnModel colModel = table.getColumnModel(); + colModel.getColumn(0).setPreferredWidth(30); + colModel.getColumn(0).setMaxWidth(40); + colModel.getColumn(1).setPreferredWidth(50); + colModel.getColumn(1).setMaxWidth(70); + colModel.getColumn(2).setPreferredWidth(40); + colModel.getColumn(2).setMaxWidth(60); + colModel.getColumn(3).setPreferredWidth(40); + colModel.getColumn(3).setMaxWidth(60); + colModel.getColumn(4).setPreferredWidth(40); + colModel.getColumn(4).setMaxWidth(60); + colModel.getColumn(5).setPreferredWidth(40); + colModel.getColumn(5).setMaxWidth(60); + } + + table.getSelectionModel().setSelectionMode( + ListSelectionModel.SINGLE_SELECTION); + + table.getSelectionModel().addListSelectionListener( + new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + final int selectedRow = table.getSelectedRow(); + selectionModel.setSelectedIndex(selectedRow); + + if (!getSelectedSymbol().isPresent()) { + return; + } + + final Box boundingBox = getSelectedSymbol().get().getBoundingBox(); + + final Rectangle scaled = new Rectangle( + scaled(boundingBox.getX() - 10, scale.current()), + scaled(boundingBox.getY() - 10, scale.current()), + scaled(boundingBox.getWidth() + 10, scale.current()), + scaled(boundingBox.getHeight() + 10, scale.current())); + + lblCanvas.scrollRectToVisible(scaled); + + Rectangle cell = tabSymbols.getTable().getCellRect( + selectedRow, 0, true); + tabSymbols.getTable().scrollRectToVisible(cell); + + renderer.render(model, scale.current()); + } + }); + + JPanel toolbar = new JPanel(); + toolbar.setBorder(new EmptyBorder(0, 4, 4, 4)); + toolbar.setBackground(UIManager.getColor("window")); + add(toolbar, BorderLayout.NORTH); + + JSplitPane splitMain = new JSplitPane(); + add(splitMain, BorderLayout.CENTER); + GridBagLayout gbl_toolbar = new GridBagLayout(); + gbl_toolbar.columnWidths = new int[]{0, 56, 15, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 36, 0, 0}; + gbl_toolbar.rowHeights = new int[]{0, 0}; + gbl_toolbar.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE}; + gbl_toolbar.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + toolbar.setLayout(gbl_toolbar); + + JLabel lblSymbol = new JLabel("Symbol"); + GridBagConstraints gbc_lblSymbol = new GridBagConstraints(); + gbc_lblSymbol.insets = new Insets(0, 0, 0, 5); + gbc_lblSymbol.anchor = GridBagConstraints.EAST; + gbc_lblSymbol.gridx = 0; + gbc_lblSymbol.gridy = 0; + toolbar.add(lblSymbol, gbc_lblSymbol); + + tfSymbol = new JTextField(); + tfSymbol.addActionListener(e -> { + final Optional symbol = getSelectedSymbol(); + + if (!symbol.isPresent()) { + return; + } + + symbol.get().setText(tfSymbol.getText()); + table.tableChanged(new TableModelEvent(table.getModel(), + table.getSelectedRow())); + + int newSel = table.getSelectedRow() + 1; + if (newSel < table.getModel().getRowCount()) { + table.getSelectionModel().setSelectionInterval(newSel, + newSel); + } + }); + + GridBagConstraints gbc_tfSymbol = new GridBagConstraints(); + gbc_tfSymbol.insets = new Insets(0, 0, 0, 5); + gbc_tfSymbol.fill = GridBagConstraints.HORIZONTAL; + gbc_tfSymbol.gridx = 1; + gbc_tfSymbol.gridy = 0; + toolbar.add(tfSymbol, gbc_tfSymbol); + tfSymbol.setColumns(6); + + Component hsDiv1 = javax.swing.Box.createHorizontalStrut(10); + GridBagConstraints gbc_hsDiv1 = new GridBagConstraints(); + gbc_hsDiv1.insets = new Insets(0, 0, 0, 5); + gbc_hsDiv1.gridx = 2; + gbc_hsDiv1.gridy = 0; + toolbar.add(hsDiv1, gbc_hsDiv1); + + JLabel lblX = new JLabel("X"); + GridBagConstraints gbc_lblX = new GridBagConstraints(); + gbc_lblX.insets = new Insets(0, 0, 0, 5); + gbc_lblX.gridx = 3; + gbc_lblX.gridy = 0; + toolbar.add(lblX, gbc_lblX); + + spinnerX = new JSpinner(); + spinnerX.setToolTipText("x coordinate"); + spinnerX.setPreferredSize(DEFAULT_SPINNER_DIMENSION); + GridBagConstraints gbc_spX = new GridBagConstraints(); + gbc_spX.insets = new Insets(0, 0, 0, 5); + gbc_spX.gridx = 4; + gbc_spX.gridy = 0; + toolbar.add(spinnerX, gbc_spX); + + JLabel lblY = new JLabel("Y"); + GridBagConstraints gbc_lblY = new GridBagConstraints(); + gbc_lblY.insets = new Insets(0, 0, 0, 5); + gbc_lblY.gridx = 5; + gbc_lblY.gridy = 0; + toolbar.add(lblY, gbc_lblY); + + spinnerY = new JSpinner(); + spinnerY.setPreferredSize(DEFAULT_SPINNER_DIMENSION); + spinnerY.setToolTipText("y coordinate"); + GridBagConstraints gbc_spY = new GridBagConstraints(); + gbc_spY.insets = new Insets(0, 0, 0, 5); + gbc_spY.gridx = 6; + gbc_spY.gridy = 0; + toolbar.add(spinnerY, gbc_spY); + + JLabel lblWidth = new JLabel("W"); + GridBagConstraints gbc_lblWidth = new GridBagConstraints(); + gbc_lblWidth.insets = new Insets(0, 0, 0, 5); + gbc_lblWidth.gridx = 7; + gbc_lblWidth.gridy = 0; + toolbar.add(lblWidth, gbc_lblWidth); + + spinnerWidth = new JSpinner(); + spinnerWidth.setToolTipText("Width"); + spinnerWidth.setPreferredSize(DEFAULT_SPINNER_DIMENSION); + GridBagConstraints gbc_spWidth = new GridBagConstraints(); + gbc_spWidth.insets = new Insets(0, 0, 0, 5); + gbc_spWidth.gridx = 8; + gbc_spWidth.gridy = 0; + toolbar.add(spinnerWidth, gbc_spWidth); + + JLabel lblHeight = new JLabel("H"); + GridBagConstraints gbc_lblHeight = new GridBagConstraints(); + gbc_lblHeight.insets = new Insets(0, 0, 0, 5); + gbc_lblHeight.gridx = 9; + gbc_lblHeight.gridy = 0; + toolbar.add(lblHeight, gbc_lblHeight); + + spinnerHeight = new JSpinner(); + spinnerHeight.setToolTipText("Height"); + spinnerHeight.setPreferredSize(DEFAULT_SPINNER_DIMENSION); + GridBagConstraints gbc_spHeight = new GridBagConstraints(); + gbc_spHeight.insets = new Insets(0, 0, 0, 5); + gbc_spHeight.gridx = 10; + gbc_spHeight.gridy = 0; + toolbar.add(spinnerHeight, gbc_spHeight); + + Component horizontalStrut = javax.swing.Box.createHorizontalStrut(10); + GridBagConstraints gbc_horizontalStrut = new GridBagConstraints(); + gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5); + gbc_horizontalStrut.gridx = 11; + gbc_horizontalStrut.gridy = 0; + toolbar.add(horizontalStrut, gbc_horizontalStrut); + + final Insets btnMargin = new Insets(2, 4, 2, 4); + + final JButton btnZoomOut = new JButton(); + btnZoomOut.setMargin(btnMargin); + btnZoomOut.setToolTipText("Zoom out"); + btnZoomOut.setBackground(Color.WHITE); + btnZoomOut.setIcon(new ImageIcon(BoxEditor.class.getResource("/icons/magnifier_zoom_out.png"))); + GridBagConstraints gbc_btnZoomOut = new GridBagConstraints(); + gbc_btnZoomOut.insets = new Insets(0, 0, 0, 5); + gbc_btnZoomOut.gridx = 12; + gbc_btnZoomOut.gridy = 0; + toolbar.add(btnZoomOut, gbc_btnZoomOut); + + final JButton btnZoomIn = new JButton(); + btnZoomIn.setMargin(btnMargin); + btnZoomIn.setToolTipText("Zoom in"); + btnZoomIn.setBackground(Color.WHITE); + btnZoomIn.setIcon(new ImageIcon(BoxEditor.class.getResource("/icons/magnifier_zoom_in.png"))); + GridBagConstraints gbc_btnZoomIn = new GridBagConstraints(); + gbc_btnZoomIn.gridx = 13; + gbc_btnZoomIn.gridy = 0; + toolbar.add(btnZoomIn, gbc_btnZoomIn); + + btnZoomOut.addActionListener(evt -> { + if (scale.hasPrevious()) { + renderer.render(getBoxFileModel(), scale.previous()); + } + + if (!scale.hasPrevious()) { + btnZoomOut.setEnabled(false); + } + + btnZoomIn.setEnabled(true); + }); + + btnZoomIn.addActionListener(evt -> { + if (scale.hasNext()) { + renderer.render(getBoxFileModel(), scale.next()); + } + + if (!scale.hasNext()) { + btnZoomIn.setEnabled(false); + } + + btnZoomOut.setEnabled(true); + }); + + Dimension tabSize = new Dimension(260, 0); + tabSymbols.setMinimumSize(tabSize); + tabSymbols.setPreferredSize(tabSize); + tabSymbols.setMaximumSize(tabSize); + splitMain.setLeftComponent(tabSymbols); + + JScrollPane scrollPane = new JScrollPane(); + splitMain.setRightComponent(scrollPane); + + lblCanvas = new JLabel(""); + scrollPane.setViewportView(lblCanvas); + + contextMenu = new JPopupMenu("Box operations"); + contextMenu.add(new JMenuItem("Split box")); + contextMenu.add(new JSeparator()); + contextMenu.add(new JMenuItem("Merge with previous box")); + contextMenu.add(new JMenuItem("Merge with next box")); + + lblCanvas.addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + clicked(e); + } + + public void mouseReleased(MouseEvent e) { + clicked(e); + } + + private void clicked(MouseEvent e) { + if (!model.isPresent()) { + // ignore clicks if no model is present + return; + } + + final Point p = new Point(unscaled(e.getX(), scale.current()), + unscaled(e.getY(), scale.current())); + + final Iterator it = + model.get().getBoxes().iterator(); + + final ListSelectionModel sel = + tabSymbols.getTable().getSelectionModel(); + + boolean selection = false; + for (int i = 0; it.hasNext(); i++) { + final Box boundingBox = it.next().getBoundingBox(); + + if (boundingBox.contains(p)) { + selection = true; + selectionModel.setSelectedIndex(i); + sel.setSelectionInterval(i, i); + break; + } + } + + if (!selection) { + selectionModel.setSelectedIndex(-1); + sel.setSelectionInterval(-1, -1); + } else if (e.isPopupTrigger()) { + contextMenu.show(e.getComponent(), e.getX(), e.getY()); + } + + renderer.render(model, scale.current()); + } + }); + + selectionModel.addSelectionListener(index -> { + if (index < 0) { + return; + } + + final SymbolTableModel tabModel = + (SymbolTableModel) tabSymbols.getTable().getModel(); + final Symbol symbol = tabModel.getSymbol(index); + + final String symbolText = symbol.getText(); + tfSymbol.setText(symbolText); + + // tooltip with codePoints + final StringBuilder tooltip = new StringBuilder("[ "); + for (int i = 0; i < symbolText.length(); i++) { + tooltip.append(Integer.toHexString(symbolText.codePointAt(i))) + .append(' '); + } + tfSymbol.setToolTipText(tooltip.append(']').toString()); + + final Box boundingBox = symbol.getBoundingBox(); + spinnerX.setValue(boundingBox.getX()); + spinnerY.setValue(boundingBox.getY()); + spinnerWidth.setValue(boundingBox.getWidth()); + spinnerHeight.setValue(boundingBox.getHeight()); + + lblCanvas.scrollRectToVisible(boundingBox.toRectangle()); + }); + + spinnerX.addPropertyChangeListener(spinnerListener); + spinnerY.addPropertyChangeListener(spinnerListener); + spinnerWidth.addPropertyChangeListener(spinnerListener); + spinnerHeight.addPropertyChangeListener(spinnerListener); + } + + @Override + public void setBoxFileModel(Optional model) { + this.model = model; + + final SymbolTableModel tabModel = + (SymbolTableModel) tabSymbols.getTable().getModel(); + + final DefaultListModel source = + (DefaultListModel) tabModel.getSource().getSource(); + + source.clear(); + + if (model.isPresent()) { + // fill table model and render the page + model.get().getBoxes().forEach(source::addElement); + } + + renderer.render(model, scale.current()); + } + + @Override + public void setPageModel(Optional model) { + if (model.isPresent()) { + setBoxFileModel(Optional.of(model.get().toBoxFileModel())); + pageModel = model; + } else { + setBoxFileModel(Optional.empty()); + pageModel = model; + } + } + + @Override + public Optional getBoxFileModel() { + return model; + } + + @Override + public Optional getPageModel() { + return pageModel; + } + + public JLabel getCanvas() { + return lblCanvas; + } + + public FilteredTable getSymbols() { + return tabSymbols; + } + + @Override + public Component asComponent() { + return this; + } + + public Optional getSelectedSymbol() { + final int index = tabSymbols.getTable().getSelectedRow(); + + if (index < 0) { + return Optional.empty(); + } + + return Optional.of(((SymbolTableModel) tabSymbols.getTable().getModel()) + .getSymbol(index)); + } + + public JTextField getSymbolTextField() { + return tfSymbol; + } + + public JSpinner getXSpinner() { + return spinnerX; + } + + public JSpinner getYSpinner() { + return spinnerY; + } + + public JSpinner getWidthSpinner() { + return spinnerWidth; + } + + public JSpinner getHeightSpinner() { + return spinnerHeight; + } + + public void addChangeListener(ChangeListener listener) { + changeListeners.add(listener); + } + + public void removeChangeListener(ChangeListener listener) { + changeListeners.remove(listener); + } + + public boolean hasChanged() { + return changed; + } + + public void setChanged(boolean b) { + changed = b; + + final ChangeEvent evt = new ChangeEvent(this); + for (ChangeListener l : changeListeners) { + l.stateChanged(evt); + } + } + + @Override + public void freeResources() { + lblCanvas.setIcon(null); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java new file mode 100644 index 00000000..827d5e13 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileCanvas.java @@ -0,0 +1,70 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.ListModel; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.util.Optional; + +import static de.vorb.tesseract.gui.model.Scale.scaled; + +public class BoxFileCanvas extends Canvas { + private static final long serialVersionUID = 1L; + + private float scale; + private Optional> symbols = + Optional.empty(); + private int selectedIndex = -1; + + public BoxFileCanvas() { + } + + public void setScale(float scale) { + this.scale = scale; + } + + public void setSymbols(Optional> symbols, + int selectedIndex) { + this.symbols = symbols; + this.selectedIndex = -1; + } + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + if (!this.symbols.isPresent()) + return; + + final ListModel symbols = this.symbols.get(); + final int size = symbols.getSize(); + + for (int i = 0; i < size; i++) { + final Symbol symbol = symbols.getElementAt(i); + drawSymbolBox((Graphics2D) g, symbol, scale, i == selectedIndex); + } + } + + private void drawSymbolBox(final Graphics2D g, final Symbol symbol, + final float scale, final boolean isSelected) { + final Box boundingBox = symbol.getBoundingBox(); + + // set selected colors + if (isSelected) { + g.setColor(Colors.SELECTION); + g.setStroke(Strokes.SELECTION); + } + + // draw the box + g.drawRect(scaled(boundingBox.getX(), scale), scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), scaled(boundingBox.getHeight(), scale)); + + // unset selected colors + if (isSelected) { + g.setColor(Colors.NORMAL); + g.setStroke(Strokes.NORMAL); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java new file mode 100644 index 00000000..8b08d69b --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/BoxFileModelComponent.java @@ -0,0 +1,9 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.BoxFileModel; + +import java.util.Optional; + +public interface BoxFileModelComponent extends PageModelComponent { + void setBoxFileModel(Optional model); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java new file mode 100644 index 00000000..df2d98fc --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Canvas.java @@ -0,0 +1,48 @@ +package de.vorb.tesseract.gui.view; + +import javax.swing.JComponent; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Rectangle; +import java.util.Optional; + +public class Canvas extends JComponent { + private static final long serialVersionUID = 1L; + + private static final Dimension DIM_EMPTY = new Dimension(0, 0); + + private Optional image = Optional.empty(); + + public Canvas() { + } + + public void setImage(Optional image) { + this.image = image; + } + + @Override + public void paintComponent(Graphics g) { + final Rectangle rect = getVisibleRect(); + + if (image.isPresent()) { + g.setClip(rect.x, rect.y, rect.width, rect.height); + g.drawImage(image.get(), 0, 0, null); + } else { + g.setColor(Color.WHITE); + g.fillRect((int) rect.getX(), (int) rect.getY(), + (int) rect.getWidth(), (int) rect.getHeight()); + } + } + + @Override + public Dimension getPreferredSize() { + if (!image.isPresent()) { + return DIM_EMPTY; + } + + return new Dimension(image.get().getWidth(null), image.get().getHeight( + null)); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java new file mode 100644 index 00000000..a05e1866 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Colors.java @@ -0,0 +1,18 @@ +package de.vorb.tesseract.gui.view; + +import java.awt.Color; + +public final class Colors { + + private Colors() {} + + public static final Color NORMAL = new Color(0xCC0000FF, true); + public static final Color SELECTION = new Color(0xCCFF0000, true); + public static final Color SELECTION_BG = new Color(0x44FF0000, true); + + public static final Color BASELINE = Color.BLUE; + public static final Color TEXT = Color.BLACK; + public static final Color LINE_NUMBER = Color.GRAY; + public static final Color BLOCK = Color.GREEN; + public static final Color PARAGRAPH = Color.ORANGE; +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java similarity index 97% rename from src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java index b8aa8fdd..6621baf5 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/DictionaryPane.java @@ -1,13 +1,10 @@ package de.vorb.tesseract.gui.view; -import javax.swing.JPanel; - -import java.awt.BorderLayout; - +import javax.swing.JButton; import javax.swing.JList; +import javax.swing.JPanel; import javax.swing.border.EmptyBorder; -import javax.swing.JButton; - +import java.awt.BorderLayout; import java.awt.FlowLayout; public class DictionaryPane extends JPanel { @@ -49,7 +46,7 @@ public DictionaryPane() { add(panel_3, BorderLayout.CENTER); panel_3.setLayout(new BorderLayout(0, 0)); - list = new JList(); + list = new JList<>(); panel_3.add(list); } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java new file mode 100644 index 00000000..e662d406 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/EvaluationPane.java @@ -0,0 +1,243 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.model.Scale; +import de.vorb.tesseract.gui.view.renderer.EvaluationPaneRenderer; + +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Optional; + +public class EvaluationPane extends JPanel implements PageModelComponent { + private static final long serialVersionUID = 1L; + + private static final Insets BUTTON_MARGIN = new Insets(2, 4, 2, 4); + + private final Scale scale; + private final EvaluationPaneRenderer renderer; + + private Optional model; + private final JLabel lblOriginal; + private final JTextArea textAreaTranscript; + private final JButton btnSaveTranscription; + private final JButton btnGenerateReport; + + private final JButton btnZoomOut; + private final JButton btnZoomIn; + private final JButton btnUseOcrResult; + + /** + * Create the panel. + * + * @param scale + */ + public EvaluationPane(final Scale scale, final String editorFont) { + setLayout(new BorderLayout(0, 0)); + + this.scale = scale; + renderer = new EvaluationPaneRenderer(this); + + JSplitPane splitPane = new JSplitPane(); + add(splitPane, BorderLayout.CENTER); + splitPane.setResizeWeight(0.5); + + JScrollPane scrollPane = new JScrollPane(); + splitPane.setLeftComponent(scrollPane); + + JLabel lblOriginalTitle = new JLabel("Original"); + lblOriginalTitle.setBorder(new EmptyBorder(0, 4, 0, 0)); + scrollPane.setColumnHeaderView(lblOriginalTitle); + + lblOriginal = new JLabel(""); + scrollPane.setViewportView(lblOriginal); + + JScrollPane scrollPane_1 = new JScrollPane(); + splitPane.setRightComponent(scrollPane_1); + + textAreaTranscript = new JTextArea(); + textAreaTranscript.setEnabled(false); + scrollPane_1.setViewportView(textAreaTranscript); + setEditorFont(editorFont); + + JLabel lblReferenceText = new JLabel("Transcription"); + lblReferenceText.setBorder(new EmptyBorder(0, 4, 0, 0)); + scrollPane_1.setColumnHeaderView(lblReferenceText); + + JPanel panel_1 = new JPanel(); + FlowLayout flowLayout = (FlowLayout) panel_1.getLayout(); + flowLayout.setAlignment(FlowLayout.RIGHT); + add(panel_1, BorderLayout.SOUTH); + + btnSaveTranscription = new JButton("Save"); + btnSaveTranscription.setEnabled(false); + btnSaveTranscription.setToolTipText("Save Transcription"); + panel_1.add(btnSaveTranscription); + + btnGenerateReport = new JButton("Generate Report"); + btnGenerateReport.setIcon(new ImageIcon( + EvaluationPane.class.getResource("/icons/report.png"))); + btnGenerateReport.setEnabled(false); + panel_1.add(btnGenerateReport); + + JPanel panel_2 = new JPanel(); + panel_2.setBorder(new EmptyBorder(0, 4, 2, 4)); + add(panel_2, BorderLayout.NORTH); + panel_2.setBackground(Color.WHITE); + GridBagLayout gbl_panel_2 = new GridBagLayout(); + gbl_panel_2.columnWidths = new int[]{67, 29, 0, 0, 0, 0}; + gbl_panel_2.rowHeights = new int[]{25, 0}; + gbl_panel_2.columnWeights = new double[]{1.0, 0.0, 0.0, 0.0, 1.0, + Double.MIN_VALUE}; + gbl_panel_2.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + panel_2.setLayout(gbl_panel_2); + + JLabel lblOcrevaluation = new JLabel("ocrevalUAtion"); + lblOcrevaluation.setFont(new Font("Tahoma", Font.PLAIN, 12)); + GridBagConstraints gbc_lblOcrevaluation = new GridBagConstraints(); + gbc_lblOcrevaluation.anchor = GridBagConstraints.WEST; + gbc_lblOcrevaluation.insets = new Insets(0, 0, 0, 5); + gbc_lblOcrevaluation.gridx = 0; + gbc_lblOcrevaluation.gridy = 0; + panel_2.add(lblOcrevaluation, gbc_lblOcrevaluation); + + btnZoomOut = new JButton(); + btnZoomOut.setAlignmentX(Component.RIGHT_ALIGNMENT); + btnZoomOut.setMargin(BUTTON_MARGIN); + btnZoomOut.setBackground(Color.WHITE); + btnZoomOut.setIcon(new ImageIcon( + EvaluationPane.class.getResource("/icons/zoom_out.png"))); + GridBagConstraints gbc_btnZoomOut = new GridBagConstraints(); + gbc_btnZoomOut.anchor = GridBagConstraints.EAST; + gbc_btnZoomOut.insets = new Insets(0, 0, 0, 5); + gbc_btnZoomOut.gridx = 1; + gbc_btnZoomOut.gridy = 0; + panel_2.add(btnZoomOut, gbc_btnZoomOut); + + btnZoomOut.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (scale.hasPrevious()) { + renderer.render(getPageModel(), scale.previous()); + } + + if (!scale.hasPrevious()) { + btnZoomOut.setEnabled(false); + } + + btnZoomIn.setEnabled(true); + } + }); + + btnZoomIn = new JButton(); + btnZoomIn.setAlignmentX(Component.RIGHT_ALIGNMENT); + btnZoomIn.setMargin(BUTTON_MARGIN); + btnZoomIn.setBackground(Color.WHITE); + btnZoomIn.setIcon(new ImageIcon( + EvaluationPane.class.getResource("/icons/zoom_in.png"))); + GridBagConstraints gbc_btnZoomIn = new GridBagConstraints(); + gbc_btnZoomIn.insets = new Insets(0, 0, 0, 5); + gbc_btnZoomIn.anchor = GridBagConstraints.WEST; + gbc_btnZoomIn.gridx = 2; + gbc_btnZoomIn.gridy = 0; + panel_2.add(btnZoomIn, gbc_btnZoomIn); + + Component horizontalStrut = Box.createHorizontalStrut(30); + GridBagConstraints gbc_horizontalStrut = new GridBagConstraints(); + gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5); + gbc_horizontalStrut.gridx = 3; + gbc_horizontalStrut.gridy = 0; + panel_2.add(horizontalStrut, gbc_horizontalStrut); + + btnUseOcrResult = new JButton("Use OCR Result"); + btnUseOcrResult.setBackground(Color.WHITE); + GridBagConstraints gbc_btnUseOcrResult = new GridBagConstraints(); + gbc_btnUseOcrResult.anchor = GridBagConstraints.EAST; + gbc_btnUseOcrResult.gridx = 4; + gbc_btnUseOcrResult.gridy = 0; + panel_2.add(btnUseOcrResult, gbc_btnUseOcrResult); + + btnZoomIn.addActionListener(evt -> { + if (scale.hasNext()) { + renderer.render(getPageModel(), scale.next()); + } + + if (!scale.hasNext()) { + btnZoomIn.setEnabled(false); + } + + btnZoomOut.setEnabled(true); + }); + } + + @Override + public Component asComponent() { + return this; + } + + public JButton getGenerateReportButton() { + return btnGenerateReport; + } + + public JButton getSaveTranscriptionButton() { + return btnSaveTranscription; + } + + public JButton getUseOCRResultButton() { + return btnUseOcrResult; + } + + public JLabel getOriginal() { + return lblOriginal; + } + + public JTextArea getTextAreaTranscript() { + return textAreaTranscript; + } + + @Override + public void setPageModel(Optional model) { + this.model = model; + + renderer.render(model, scale.current()); + } + + @Override + public Optional getPageModel() { + return model; + } + + @Override + public void freeResources() { + lblOriginal.setIcon(null); + } + + @Override + public Optional getBoxFileModel() { + if (model.isPresent()) { + return Optional.of(model.get().toBoxFileModel()); + } else { + return Optional.empty(); + } + } + + public void setEditorFont(String editorFont) { + textAreaTranscript.setFont(new Font(editorFont, Font.PLAIN, 13)); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java new file mode 100644 index 00000000..52142fa1 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FeatureDebugger.java @@ -0,0 +1,115 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.tools.training.IntClass; +import de.vorb.tesseract.tools.training.IntTemplates; +import de.vorb.tesseract.util.feat.Feature3D; +import de.vorb.tesseract.util.feat.Feature4D; + +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Window; +import java.awt.geom.Line2D; +import java.awt.geom.Line2D.Double; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Optional; + +public class FeatureDebugger extends JDialog { + private static final long serialVersionUID = 1L; + + private static final double PI2 = Math.PI + Math.PI; + private static final double FEATURE_RADIUS = 3.5d; + private static final int WIDTH = 256; + private static final int HEIGHT = 256; + + private final JPanel contentPanel = new JPanel(); + private final BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT, + BufferedImage.TYPE_INT_RGB); + private final JLabel lblCanvas; + + private List features; + private Optional prototypes = Optional.empty(); + + /** + * Create the dialog. + * + * @param parent + */ + public FeatureDebugger(Window parent) { + super(parent); + setLocationByPlatform(true); + setTitle("Features"); + setModalityType(ModalityType.APPLICATION_MODAL); + + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(new BorderLayout(0, 0)); + { + lblCanvas = new JLabel(new ImageIcon(canvas)); + final Dimension size = new Dimension(WIDTH, HEIGHT); + lblCanvas.setPreferredSize(size); + contentPanel.add(lblCanvas); + } + + pack(); + } + + public void setFeatures(List features) { + this.features = features; + redraw(); + } + + public void setPrototypes(Optional prototypes) { + this.prototypes = prototypes; + } + + private void redraw() { + final Graphics2D g2d = canvas.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setColor(Color.BLACK); + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, WIDTH, HEIGHT); + + final Double line = new Line2D.Double(0d, 0d, 0d, 0d); + if (prototypes.isPresent()) { + final List classes = prototypes.get().getClasses(); + for (final Feature4D feat : classes.get(0).getProtoSets().get(0).getProtos()) { + // transform the angle back to radians + final double angle = feat.getAngle() / 256d * PI2; + final double dx = Math.cos(angle) * feat.getC(); + final double dy = Math.sin(angle) * feat.getC(); + final double x1 = feat.getA(); + final double x2 = feat.getA() + dx; + final double y1 = feat.getB(); + final double y2 = feat.getB() + dy; + line.setLine(x1, y1, x2, y2); + g2d.draw(line); + } + } + + for (final Feature3D feat : features) { + // transform the angle back to radians + final double theta = feat.getTheta() / 256d * PI2; + final double dx = Math.cos(theta) * FEATURE_RADIUS; + final double dy = Math.sin(theta) * FEATURE_RADIUS; + final double x1 = feat.getX() - dx; + final double x2 = feat.getX() + dx; + final double y1 = feat.getY() - dy; + final double y2 = feat.getY() + dy; + line.setLine(x1, y1, x2, y2); + g2d.draw(line); + } + g2d.dispose(); + lblCanvas.repaint(); + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java similarity index 85% rename from src/main/java/de/vorb/tesseract/gui/view/FilteredList.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java index 32b1cb74..1062fb53 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredList.java @@ -1,9 +1,10 @@ package de.vorb.tesseract.gui.view; -import java.awt.BorderLayout; -import java.awt.SystemColor; +import de.vorb.tesseract.gui.model.FilteredListModel; +import de.vorb.tesseract.gui.util.FilterProvider; import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -11,11 +12,8 @@ import javax.swing.ListModel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; - -import com.google.common.base.Optional; - -import de.vorb.tesseract.gui.model.FilteredListModel; -import de.vorb.tesseract.gui.model.FilteredListModel.Filter; +import java.awt.BorderLayout; +import java.awt.SystemColor; public class FilteredList extends JPanel { private static final long serialVersionUID = 1L; @@ -23,10 +21,6 @@ public class FilteredList extends JPanel { private final JList list; private final SearchField filterField; - public static interface FilterProvider { - Optional> getFilter(String filterText); - } - /** * Create the panel. */ @@ -39,7 +33,8 @@ public FilteredList(final FilterProvider filterProvider) { add(scrollPane, BorderLayout.CENTER); - list = new JList(); + list = new JList<>( + new FilteredListModel<>(new DefaultListModel<>())); scrollPane.setViewportView(list); filterField = new SearchField(); @@ -64,6 +59,7 @@ public void changedUpdate(DocumentEvent evt) { private void filter() { final ListModel model = list.getModel(); + if (!(model instanceof FilteredListModel)) { return; } @@ -75,7 +71,7 @@ private void filter() { filterField.getTextField().getText(); filteredModel.setFilter( - filterProvider.getFilter(query)); + filterProvider.getFilterFor(query)); } }); } @@ -87,4 +83,9 @@ public JList getList() { public JTextField getTextField() { return filterField.getTextField(); } + + public DefaultListModel getListModel() { + return (DefaultListModel) ((FilteredListModel) getList() + .getModel()).getSource(); + } } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java new file mode 100644 index 00000000..b1e619c4 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/FilteredTable.java @@ -0,0 +1,93 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.FilteredTableModel; +import de.vorb.tesseract.gui.util.FilterProvider; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.ListModel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.TableModel; +import java.awt.BorderLayout; +import java.awt.SystemColor; + +public class FilteredTable extends JPanel { + private static final long serialVersionUID = 1L; + + private final JTable table; + private final SearchField filterField; + + /** + * Create the panel. + */ + public FilteredTable(final FilteredTableModel model, + final FilterProvider filterProvider) { + setLayout(new BorderLayout(0, 0)); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setBorder(BorderFactory.createEtchedBorder()); + scrollPane.setBackground(SystemColor.window); + + add(scrollPane, BorderLayout.CENTER); + + table = new JTable(model); + table.setFillsViewportHeight(true); + scrollPane.setViewportView(table); + + filterField = new SearchField(); + add(filterField, BorderLayout.SOUTH); + + filterField.getTextField().getDocument().addDocumentListener( + new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent evt) { + filter(); + } + + @Override + public void insertUpdate(DocumentEvent evt) { + filter(); + } + + @Override + public void changedUpdate(DocumentEvent evt) { + filter(); + } + + // ignore unchecked behavior and make it fail at runtime + @SuppressWarnings("unchecked") + private void filter() { + final TableModel model = table.getModel(); + if (!(model instanceof FilteredTableModel)) { + return; + } + + final FilteredTableModel filteredModel = + (FilteredTableModel) model; + + final String query = + filterField.getTextField().getText(); + + filteredModel.getSource().setFilter( + filterProvider.getFilterFor(query)); + } + }); + } + + public JTable getTable() { + return table; + } + + public JTextField getTextField() { + return filterField.getTextField(); + } + + @SuppressWarnings("unchecked") + public ListModel getListModel() { + return ((FilteredTableModel) table.getModel()).getSource(); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java new file mode 100644 index 00000000..fcc9d1e4 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/ImageModelComponent.java @@ -0,0 +1,11 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.ImageModel; + +import java.util.Optional; + +public interface ImageModelComponent extends MainComponent { + void setImageModel(Optional model); + + Optional getImageModel(); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java new file mode 100644 index 00000000..0723c193 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java @@ -0,0 +1,9 @@ +package de.vorb.tesseract.gui.view; + +import java.awt.Component; + +public interface MainComponent { + Component asComponent(); + + void freeResources(); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java new file mode 100644 index 00000000..e2c7249b --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 1998, 2007, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + */ + +package de.vorb.tesseract.gui.view; + +import sun.swing.DefaultLookup; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.Icon; +import javax.swing.JList; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import java.awt.Color; +import java.awt.Component; +import java.nio.file.Path; + +/** + * @author Paul Vorbach + * @deprecated + */ +public class PageListCellRenderer extends DefaultListCellRenderer { + private static final long serialVersionUID = 1L; + + /** + * An empty Border. This field might not be used. To change the + * Border used by this renderer override the + * getListCellRendererComponent method and set the border of the + * returned component directly. + */ + private static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1); + private static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1, + 1, 1); + protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER; + + private Border getNoFocusBorder() { + Border border = DefaultLookup.getBorder(this, ui, "List.cellNoFocusBorder"); + if (System.getSecurityManager() != null) { + if (border != null) + return border; + return SAFE_NO_FOCUS_BORDER; + } else { + if (border != null && + (noFocusBorder == null || + noFocusBorder == DEFAULT_NO_FOCUS_BORDER)) { + return border; + } + return noFocusBorder; + } + } + + public Component getListCellRendererComponent( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + setComponentOrientation(list.getComponentOrientation()); + + Color bg = null; + Color fg = null; + + JList.DropLocation dropLocation = list.getDropLocation(); + if (dropLocation != null + && !dropLocation.isInsert() + && dropLocation.getIndex() == index) { + + bg = DefaultLookup.getColor(this, ui, "List.dropCellBackground"); + fg = DefaultLookup.getColor(this, ui, "List.dropCellForeground"); + + isSelected = true; + } + + if (isSelected) { + setBackground(bg == null ? list.getSelectionBackground() : bg); + setForeground(fg == null ? list.getSelectionForeground() : fg); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + if (value instanceof Icon) { + setIcon((Icon) value); + setText(""); + } else if (value instanceof Path) { + setText(((Path) value).getFileName().toString()); + } else { + setIcon(null); + setText((value == null) ? "" : value.toString()); + } + + setEnabled(list.isEnabled()); + setFont(list.getFont()); + + Border border = null; + if (cellHasFocus) { + if (isSelected) { + border = DefaultLookup.getBorder(this, ui, + "List.focusSelectedCellHighlightBorder"); + } + if (border == null) { + border = DefaultLookup.getBorder(this, ui, + "List.focusCellHighlightBorder"); + } + } else { + border = getNoFocusBorder(); + } + setBorder(border); + + return this; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java new file mode 100644 index 00000000..f46930e2 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageModelComponent.java @@ -0,0 +1,14 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.PageModel; + +import java.util.Optional; + +public interface PageModelComponent extends MainComponent { + void setPageModel(Optional model); + + Optional getPageModel(); + + Optional getBoxFileModel(); +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/PageView.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java similarity index 84% rename from src/main/java/de/vorb/tesseract/gui/view/PageView.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java index f2733dcf..d624ae18 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/PageView.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PageView.java @@ -1,9 +1,9 @@ package de.vorb.tesseract.gui.view; -import com.google.common.base.Optional; - import de.vorb.tesseract.gui.model.PageModel; +import java.util.Optional; + public interface PageView { void setPageModel(Optional pageModel); diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java new file mode 100644 index 00000000..47e286bc --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/PreprocessingPane.java @@ -0,0 +1,325 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.ImageModel; +import de.vorb.tesseract.tools.preprocessing.DefaultPreprocessor; +import de.vorb.tesseract.tools.preprocessing.Preprocessor; +import de.vorb.tesseract.tools.preprocessing.binarization.Binarization; +import de.vorb.tesseract.tools.preprocessing.binarization.BinarizationMethod; +import de.vorb.tesseract.tools.preprocessing.binarization.Otsu; +import de.vorb.tesseract.tools.preprocessing.binarization.Sauvola; +import de.vorb.tesseract.tools.preprocessing.filter.BlobSizeFilter; +import de.vorb.tesseract.tools.preprocessing.filter.ImageFilter; + +import javax.swing.BoxLayout; +import javax.swing.DefaultComboBoxModel; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JSplitPane; +import javax.swing.SpinnerNumberModel; +import javax.swing.UIManager; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +public class PreprocessingPane extends JPanel implements ImageModelComponent { + private static final long serialVersionUID = 1L; + + private final JComboBox comboBinarization; + private final JSpinner spinnerWindowRadius; + + private final JSpinner spinnerBlobMinSize; + private final JSpinner spinnerBlobMaxSize; + + private final JButton btnPreview; + private final JLabel lblPreview; + + private final JButton btnApplyToPage; + private final JButton btnApplyToAllPages; + + private Optional imageModel = Optional.empty(); + + /** + * Create the panel. + */ + public PreprocessingPane() { + setBackground(Color.WHITE); + setLayout(new BorderLayout(0, 0)); + + JSplitPane splitPane = new JSplitPane(); + add(splitPane); + + JPanel panel_3 = new JPanel(); + splitPane.setLeftComponent(panel_3); + panel_3.setLayout(new BoxLayout(panel_3, BoxLayout.Y_AXIS)); + + JPanel panel = new JPanel(); + panel_3.add(panel); + panel.setBackground(Color.WHITE); + panel.setBorder(new CompoundBorder(new TitledBorder( + UIManager.getBorder("TitledBorder.border"), "Binarization", + TitledBorder.LEADING, TitledBorder.TOP, null, + new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4))); + GridBagLayout gbl_panel = new GridBagLayout(); + gbl_panel.columnWidths = new int[]{0, 0, 0}; + gbl_panel.rowHeights = new int[]{0, 0, 0}; + gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE}; + gbl_panel.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + panel.setLayout(gbl_panel); + + JLabel lblMethod = new JLabel("Method"); + GridBagConstraints gbc_lblMethod = new GridBagConstraints(); + gbc_lblMethod.insets = new Insets(0, 0, 5, 5); + gbc_lblMethod.anchor = GridBagConstraints.EAST; + gbc_lblMethod.gridx = 0; + gbc_lblMethod.gridy = 0; + panel.add(lblMethod, gbc_lblMethod); + + comboBinarization = new JComboBox<>(); + comboBinarization.setBackground(Color.WHITE); + comboBinarization.setModel(new DefaultComboBoxModel<>( + BinarizationMethod.values())); + comboBinarization.setSelectedIndex(0); + GridBagConstraints gbc_cbBinarization = new GridBagConstraints(); + gbc_cbBinarization.insets = new Insets(0, 0, 5, 0); + gbc_cbBinarization.fill = GridBagConstraints.HORIZONTAL; + gbc_cbBinarization.gridx = 1; + gbc_cbBinarization.gridy = 0; + panel.add(comboBinarization, gbc_cbBinarization); + + comboBinarization.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (comboBinarization.getSelectedItem() == BinarizationMethod.SAUVOLA) { + spinnerWindowRadius.setEnabled(true); + } else { + spinnerWindowRadius.setEnabled(false); + } + } + }); + + JLabel lblWindowRadius = new JLabel("Window Radius"); + GridBagConstraints gbc_lblWindowSize = new GridBagConstraints(); + gbc_lblWindowSize.insets = new Insets(0, 0, 0, 5); + gbc_lblWindowSize.gridx = 0; + gbc_lblWindowSize.gridy = 1; + panel.add(lblWindowRadius, gbc_lblWindowSize); + + spinnerWindowRadius = new JSpinner(); + spinnerWindowRadius.setEnabled(false); + spinnerWindowRadius.setModel(new SpinnerNumberModel(15, 5, 50, 1)); + GridBagConstraints gbc_spinner = new GridBagConstraints(); + gbc_spinner.fill = GridBagConstraints.HORIZONTAL; + gbc_spinner.gridx = 1; + gbc_spinner.gridy = 1; + panel.add(spinnerWindowRadius, gbc_spinner); + + JPanel panel_1 = new JPanel(); + panel_3.add(panel_1); + panel_1.setBackground(Color.WHITE); + panel_1.setBorder(new CompoundBorder(new TitledBorder( + UIManager.getBorder("TitledBorder.border"), "Filters", + TitledBorder.LEADING, TitledBorder.TOP, null, + new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4))); + GridBagLayout gbl_panel_1 = new GridBagLayout(); + gbl_panel_1.columnWidths = new int[]{0, 0, 0}; + gbl_panel_1.rowHeights = new int[]{0, 0, 0}; + gbl_panel_1.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + gbl_panel_1.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + panel_1.setLayout(gbl_panel_1); + + JLabel lblBlobMinSizeFilter = new JLabel("Blob size filter (min)"); + lblBlobMinSizeFilter.setBackground(Color.WHITE); + GridBagConstraints gbc_lblBlobMinSizeFilter = new GridBagConstraints(); + gbc_lblBlobMinSizeFilter.anchor = GridBagConstraints.EAST; + gbc_lblBlobMinSizeFilter.insets = new Insets(0, 0, 5, 5); + gbc_lblBlobMinSizeFilter.gridx = 0; + gbc_lblBlobMinSizeFilter.gridy = 0; + panel_1.add(lblBlobMinSizeFilter, gbc_lblBlobMinSizeFilter); + + spinnerBlobMinSize = new JSpinner(); + spinnerBlobMinSize.setToolTipText("If this value is 0, it is ignored"); + spinnerBlobMinSize.setModel(new SpinnerNumberModel(0, 0, 300, 1)); + GridBagConstraints gbc_spinnerBlobMinSize = new GridBagConstraints(); + gbc_spinnerBlobMinSize.fill = GridBagConstraints.HORIZONTAL; + gbc_spinnerBlobMinSize.insets = new Insets(0, 0, 5, 0); + gbc_spinnerBlobMinSize.gridx = 1; + gbc_spinnerBlobMinSize.gridy = 0; + panel_1.add(spinnerBlobMinSize, gbc_spinnerBlobMinSize); + + JLabel lblBlobsizefiltermax = new JLabel("BlobSizeFilter (max)"); + GridBagConstraints gbc_lblBlobsizefiltermax = new GridBagConstraints(); + gbc_lblBlobsizefiltermax.anchor = GridBagConstraints.EAST; + gbc_lblBlobsizefiltermax.insets = new Insets(0, 0, 0, 5); + gbc_lblBlobsizefiltermax.gridx = 0; + gbc_lblBlobsizefiltermax.gridy = 1; + panel_1.add(lblBlobsizefiltermax, gbc_lblBlobsizefiltermax); + + spinnerBlobMaxSize = new JSpinner(); + spinnerBlobMaxSize.setToolTipText("If this value is 0, it is ignored"); + GridBagConstraints gbc_spinnerBlobMaxSize = new GridBagConstraints(); + gbc_spinnerBlobMaxSize.fill = GridBagConstraints.HORIZONTAL; + gbc_spinnerBlobMaxSize.gridx = 1; + gbc_spinnerBlobMaxSize.gridy = 1; + panel_1.add(spinnerBlobMaxSize, gbc_spinnerBlobMaxSize); + + JPanel panel_4 = new JPanel(); + panel_4.setBackground(Color.WHITE); + panel_3.add(panel_4); + panel_4.setLayout(new FlowLayout(FlowLayout.CENTER, 2, 2)); + + btnPreview = new JButton("Preview"); + btnPreview.setBackground(Color.WHITE); + panel_4.add(btnPreview); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.getHorizontalScrollBar().setUnitIncrement(10); + scrollPane.getVerticalScrollBar().setUnitIncrement(10); + splitPane.setRightComponent(scrollPane); + + lblPreview = new JLabel(); + scrollPane.setViewportView(lblPreview); + + JLabel lblPreviewHeading = new JLabel("Preview"); + lblPreviewHeading.setBorder(new EmptyBorder(0, 4, 0, 0)); + scrollPane.setColumnHeaderView(lblPreviewHeading); + + JPanel panel_2 = new JPanel(); + FlowLayout flowLayout = (FlowLayout) panel_2.getLayout(); + flowLayout.setAlignment(FlowLayout.TRAILING); + add(panel_2, BorderLayout.SOUTH); + + btnApplyToPage = new JButton("Apply to current page"); + btnApplyToPage.setIcon(new ImageIcon( + PreprocessingPane.class.getResource("/icons/page_white.png"))); + panel_2.add(btnApplyToPage); + + btnApplyToAllPages = new JButton("Apply to all pages"); + btnApplyToAllPages.setIcon(new ImageIcon( + PreprocessingPane.class.getResource("/icons/page_white_stack.png"))); + panel_2.add(btnApplyToAllPages); + + } + + public JButton getPreviewButton() { + return btnPreview; + } + + public JButton getApplyAllPagesButton() { + return btnApplyToAllPages; + } + + public JButton getApplyPageButton() { + return btnApplyToPage; + } + + public JLabel getPreviewLabel() { + return lblPreview; + } + + public Binarization getBinarization() { + final BinarizationMethod method = + (BinarizationMethod) comboBinarization.getSelectedItem(); + + final Binarization binarization; + switch (method) { + case SAUVOLA: + binarization = new Sauvola((int) spinnerWindowRadius.getValue()); + break; + case OTSU: + binarization = new Otsu(); + break; + default: + binarization = new Sauvola(); + } + + return binarization; + } + + public List getFilters() { + int min = (int) spinnerBlobMinSize.getModel().getValue(); + int max = (int) spinnerBlobMaxSize.getModel().getValue(); + + if (min == 0 && max == 0) { + return Collections.emptyList(); + } + + final ImageFilter blobSizeFilter = new BlobSizeFilter(min, max); + + final LinkedList result = new LinkedList<>(); + result.add(blobSizeFilter); + return result; + } + + public Preprocessor getPreprocessor() { + return new DefaultPreprocessor(getBinarization(), getFilters()); + } + + public void setPreprocessor(Preprocessor preprocessor) { + if (preprocessor instanceof DefaultPreprocessor) { + final DefaultPreprocessor p = (DefaultPreprocessor) preprocessor; + final Binarization b = p.getBinarization(); + if (b instanceof Sauvola) { + comboBinarization.setSelectedItem(BinarizationMethod.SAUVOLA); + spinnerWindowRadius.setValue(((Sauvola) b).getRadius()); + } else if (b instanceof Otsu) { + comboBinarization.setSelectedItem(BinarizationMethod.OTSU); + } + + for (ImageFilter f : p.getFilters()) { + if (f instanceof BlobSizeFilter) { + final BlobSizeFilter bsf = (BlobSizeFilter) f; + spinnerBlobMinSize.setValue(bsf.getMinArea()); + spinnerBlobMaxSize.setValue(bsf.getMaxArea()); + return; + } + } + + spinnerBlobMinSize.setValue(0); + spinnerBlobMaxSize.setValue(0); + } + } + + @Override + public Component asComponent() { + return this; + } + + @Override + public void setImageModel(Optional model) { + imageModel = model; + + if (model.isPresent()) { + lblPreview.setIcon(new ImageIcon(model.get().getPreprocessedImage())); + } else { + lblPreview.setIcon(null); + } + } + + @Override + public Optional getImageModel() { + return imageModel; + } + + @Override + public void freeResources() { + lblPreview.setIcon(null); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java new file mode 100644 index 00000000..1f3197e5 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/RecognitionPane.java @@ -0,0 +1,491 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.model.Scale; +import de.vorb.tesseract.gui.view.renderer.RecognitionRenderer; +import de.vorb.tesseract.util.AlternativeChoice; +import de.vorb.tesseract.util.FontAttributes; +import de.vorb.tesseract.util.Symbol; +import de.vorb.tesseract.util.Word; + +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSplitPane; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.event.MouseInputAdapter; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.GraphicsEnvironment; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.MouseEvent; +import java.util.Iterator; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; + +public class RecognitionPane extends JPanel implements PageModelComponent { + private static final long serialVersionUID = 1L; + + private static final int SCROLL_UNITS = 12; + + private final RecognitionRenderer renderer; + private final Scale scale; + + private JLabel lblOriginal_1; + private final JLabel lblOriginal; + private JLabel lblRecognition_1; + private final JLabel lblRecognition; + + private final JCheckBox cbWordBoxes; + private final JCheckBox cbSymbolBoxes; + private final JCheckBox cbLineNumbers; + private final JCheckBox cbBaselines; + + private Optional model = Optional.empty(); + + private final Timer delayer = new Timer(true); + private JButton btZoomOut; + private JButton btZoomIn; + private JCheckBox cbBlocks; + private JCheckBox cbParagraphs; + private JLabel lblFont; + private Component horizontalStrut; + private JPopupMenu popupMenu; + + /** + * Create the panel. + * + * @param scale + */ + public RecognitionPane(final Scale scale, final String renderingFont) { + setLayout(new BorderLayout(0, 0)); + + renderer = new RecognitionRenderer(this, renderingFont); + this.scale = scale; + + JSplitPane splitPane = new JSplitPane(); + splitPane.setBackground(Color.WHITE); + splitPane.setOneTouchExpandable(true); + splitPane.setEnabled(false); + splitPane.setResizeWeight(0.5); + add(splitPane, BorderLayout.CENTER); + + final JScrollPane spOriginal = new JScrollPane(); + spOriginal.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS); + spOriginal.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS); + splitPane.setLeftComponent(spOriginal); + + lblOriginal = new JLabel(); + lblOriginal.setVerticalAlignment(SwingConstants.TOP); + spOriginal.setViewportView(lblOriginal); + + lblOriginal_1 = new JLabel("Original"); + lblOriginal_1.setBorder(new EmptyBorder(0, 4, 0, 0)); + spOriginal.setColumnHeaderView(lblOriginal_1); + + final JScrollPane spHOCR = new JScrollPane(); + spHOCR.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS); + spHOCR.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS); + splitPane.setRightComponent(spHOCR); + + lblRecognition = new JLabel(); + lblRecognition.setVerticalAlignment(SwingConstants.TOP); + spHOCR.setViewportView(lblRecognition); + + final JPopupMenu popupMenu = new JPopupMenu(); + final JMenuItem glyph = new JMenuItem("Show in Box Editor"); + popupMenu.add(glyph); + + final MouseInputAdapter adapter = new MouseInputAdapter() { + @Override + public void mousePressed(MouseEvent e) { + showPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + showPopup(e); + } + + private void showPopup(MouseEvent e) { + if (!e.isPopupTrigger()) { + return; + } + + if (cbSymbolBoxes.isSelected()) { + final Optional symbol = findSymbolAt(e.getX(), + e.getY()); + + if (symbol.isPresent()) { + final Symbol s = symbol.get(); + popupMenu.removeAll(); + popupMenu.add(String.format( + "Alternative choices for symbol \"%s\" (confidence = %.2f%%):", + s.getText(), s.getConfidence())); + popupMenu.add(new JSeparator()); + + for (AlternativeChoice alt : s.getAlternatives()) { + popupMenu.add(String.format( + "- \"%s\" (confidence = %.2f%%)", + alt.getText(), + alt.getConfidence())); + } + + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } else if (cbWordBoxes.isSelected()) { + final Optional word = findWordAt(e.getX(), e.getY()); + + if (word.isPresent()) { + final Word w = word.get(); + + popupMenu.removeAll(); + popupMenu.add(String.format("Word confidence = %.2f%%", + w.getConfidence())); + popupMenu.add(new JSeparator()); + final FontAttributes fa = w.getFontAttributes(); + popupMenu.add(String.format("Font ID = %d", + fa.getFontID())); + popupMenu.add(String.format("Font size = %dpx", + fa.getSize())); + + if (fa.isBold()) { + popupMenu.add("Bold"); + } + if (fa.isItalic()) { + popupMenu.add("Italic"); + } + if (fa.isSerif()) { + popupMenu.add("Serif"); + } + if (fa.isMonospace()) { + popupMenu.add("Monospace"); + } + if (fa.isSmallCaps()) { + popupMenu.add("Small Caps"); + } + if (fa.isUnderlined()) { + popupMenu.add("Underlined"); + } + + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + }; + + lblRecognition.addMouseListener(adapter); + lblOriginal.addMouseListener(adapter); + + lblRecognition_1 = new JLabel("Recognition Result"); + lblRecognition_1.setBorder(new EmptyBorder(0, 4, 0, 0)); + spHOCR.setColumnHeaderView(lblRecognition_1); + + final ItemListener checkBoxListener = new ItemListener() { + @Override + public void itemStateChanged(ItemEvent ev) { + if (cbWordBoxes == ev.getSource() && cbWordBoxes.isSelected()) { + cbSymbolBoxes.setSelected(false); + } else if (cbSymbolBoxes == ev.getSource() + && cbSymbolBoxes.isSelected()) { + cbWordBoxes.setSelected(false); + } + + render(); + } + }; + + JPanel panel_1 = new JPanel(); + panel_1.setBorder(new EmptyBorder(0, 4, 4, 4)); + panel_1.setBackground(Color.WHITE); + add(panel_1, BorderLayout.NORTH); + GridBagLayout gbl_panel_1 = new GridBagLayout(); + gbl_panel_1.columnWidths = new int[]{83, 91, 89, 65, 0, 0, 0, 0, + 28, 0, 0, 0, + 0}; + gbl_panel_1.rowHeights = new int[]{23, 0}; + gbl_panel_1.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE}; + gbl_panel_1.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + panel_1.setLayout(gbl_panel_1); + + cbWordBoxes = new JCheckBox("Word boxes"); + cbWordBoxes.setBackground(Color.WHITE); + GridBagConstraints gbc_cbWordBoxes = new GridBagConstraints(); + gbc_cbWordBoxes.anchor = GridBagConstraints.NORTHWEST; + gbc_cbWordBoxes.insets = new Insets(0, 0, 0, 5); + gbc_cbWordBoxes.gridx = 0; + gbc_cbWordBoxes.gridy = 0; + panel_1.add(cbWordBoxes, gbc_cbWordBoxes); + cbWordBoxes.setSelected(true); + cbWordBoxes.addItemListener(checkBoxListener); + + cbSymbolBoxes = new JCheckBox("Symbol boxes"); + cbSymbolBoxes.setBackground(Color.WHITE); + GridBagConstraints gbc_cbSymbolBoxes = new GridBagConstraints(); + gbc_cbSymbolBoxes.anchor = GridBagConstraints.NORTHWEST; + gbc_cbSymbolBoxes.insets = new Insets(0, 0, 0, 5); + gbc_cbSymbolBoxes.gridx = 1; + gbc_cbSymbolBoxes.gridy = 0; + panel_1.add(cbSymbolBoxes, gbc_cbSymbolBoxes); + cbSymbolBoxes.setSelected(false); + cbSymbolBoxes.addItemListener(checkBoxListener); + + cbLineNumbers = new JCheckBox("Line numbers"); + cbLineNumbers.setBackground(Color.WHITE); + GridBagConstraints gbc_cbLineNumbers = new GridBagConstraints(); + gbc_cbLineNumbers.anchor = GridBagConstraints.NORTHWEST; + gbc_cbLineNumbers.insets = new Insets(0, 0, 0, 5); + gbc_cbLineNumbers.gridx = 2; + gbc_cbLineNumbers.gridy = 0; + panel_1.add(cbLineNumbers, gbc_cbLineNumbers); + cbLineNumbers.setSelected(true); + cbLineNumbers.addItemListener(checkBoxListener); + + cbBaselines = new JCheckBox("Baseline"); + cbBaselines.setBackground(Color.WHITE); + GridBagConstraints gbc_cbBaselines = new GridBagConstraints(); + gbc_cbBaselines.anchor = GridBagConstraints.NORTHWEST; + gbc_cbBaselines.insets = new Insets(0, 0, 0, 5); + gbc_cbBaselines.gridx = 3; + gbc_cbBaselines.gridy = 0; + panel_1.add(cbBaselines, gbc_cbBaselines); + cbBaselines.setSelected(false); + cbBaselines.addItemListener(checkBoxListener); + + cbBlocks = new JCheckBox("Blocks"); + cbBlocks.setBackground(Color.WHITE); + GridBagConstraints gbc_chckbxBlocks = new GridBagConstraints(); + gbc_chckbxBlocks.insets = new Insets(0, 0, 0, 5); + gbc_chckbxBlocks.gridx = 4; + gbc_chckbxBlocks.gridy = 0; + panel_1.add(cbBlocks, gbc_chckbxBlocks); + cbBlocks.addItemListener(checkBoxListener); + + cbParagraphs = new JCheckBox("Paragraphs"); + cbParagraphs.setBackground(Color.WHITE); + GridBagConstraints gbc_chckbxParagraphs = new GridBagConstraints(); + gbc_chckbxParagraphs.insets = new Insets(0, 0, 0, 5); + gbc_chckbxParagraphs.gridx = 5; + gbc_chckbxParagraphs.gridy = 0; + panel_1.add(cbParagraphs, gbc_chckbxParagraphs); + cbParagraphs.addItemListener(checkBoxListener); + + horizontalStrut = Box.createHorizontalStrut(20); + GridBagConstraints gbc_horizontalStrut = new GridBagConstraints(); + gbc_horizontalStrut.insets = new Insets(0, 0, 0, 5); + gbc_horizontalStrut.gridx = 6; + gbc_horizontalStrut.gridy = 0; + panel_1.add(horizontalStrut, gbc_horizontalStrut); + + final Insets btnMargin = new Insets(2, 4, 2, 4); + + btZoomOut = new JButton(new ImageIcon( + RecognitionPane.class.getResource("/icons/zoom_out.png"))); + btZoomOut.addActionListener(ev -> { + if (scale.hasPrevious()) { + renderer.render(getPageModel(), scale.previous()); + } + + if (!scale.hasPrevious()) { + btZoomOut.setEnabled(false); + } + + btZoomIn.setEnabled(true); + }); + btZoomOut.setMargin(btnMargin); + btZoomOut.setToolTipText("Zoom out"); + btZoomOut.setBackground(Color.WHITE); + GridBagConstraints gbc_btZoomOut = new GridBagConstraints(); + gbc_btZoomOut.insets = new Insets(0, 0, 0, 5); + gbc_btZoomOut.gridx = 10; + gbc_btZoomOut.gridy = 0; + panel_1.add(btZoomOut, gbc_btZoomOut); + + btZoomIn = new JButton(new ImageIcon( + RecognitionPane.class.getResource("/icons/zoom_in.png"))); + btZoomIn.addActionListener(ev -> { + if (scale.hasNext()) { + renderer.render(getPageModel(), scale.next()); + } + + if (!scale.hasPrevious()) { + btZoomIn.setEnabled(false); + } + + btZoomOut.setEnabled(true); + }); + btZoomIn.setMargin(btnMargin); + btZoomIn.setToolTipText("Zoom in"); + btZoomIn.setBackground(Color.WHITE); + GridBagConstraints gbc_btZoomIn = new GridBagConstraints(); + gbc_btZoomIn.gridx = 11; + gbc_btZoomIn.gridy = 0; + panel_1.add(btZoomIn, gbc_btZoomIn); + // comboFont.setModel(new DefaultComboBoxModel(new String[] { + // "Antiqua", "Fraktur" })); + + spOriginal.getViewport().addChangeListener(e -> { + spHOCR.getHorizontalScrollBar().setModel( + spOriginal.getHorizontalScrollBar().getModel()); + spHOCR.getVerticalScrollBar().setModel( + spOriginal.getVerticalScrollBar().getModel()); + }); + + spHOCR.getViewport().addChangeListener(e -> { + spOriginal.getHorizontalScrollBar().setModel( + spHOCR.getHorizontalScrollBar().getModel()); + spOriginal.getVerticalScrollBar().setModel( + spHOCR.getVerticalScrollBar().getModel()); + }); + } + + private Optional findSymbolAt(int x, int y) { + if (!model.isPresent()) { + return Optional.empty(); + } + + final Iterator symbolIt = + model.get().getPage().symbolIterator(); + while (symbolIt.hasNext()) { + final Symbol symb = symbolIt.next(); + final de.vorb.tesseract.util.Box bbox = symb.getBoundingBox(); + + final int scaledX0 = Scale.scaled(bbox.getX(), scale.current()); + final int scaledY0 = Scale.scaled(bbox.getY(), scale.current()); + final int scaledX1 = scaledX0 + + Scale.scaled(bbox.getWidth(), scale.current()); + final int scaledY1 = scaledY0 + + Scale.scaled(bbox.getHeight(), scale.current()); + + if (x >= scaledX0 && x <= scaledX1 && y >= scaledY0 + && y <= scaledY1) { + return Optional.of(symb); + } + } + + return Optional.empty(); + } + + private Optional findWordAt(int x, int y) { + if (!model.isPresent()) { + return Optional.empty(); + } + + final Iterator wordIt = + model.get().getPage().wordIterator(); + + while (wordIt.hasNext()) { + final Word word = wordIt.next(); + final de.vorb.tesseract.util.Box bbox = word.getBoundingBox(); + + final int scaledX0 = Scale.scaled(bbox.getX(), scale.current()); + final int scaledY0 = Scale.scaled(bbox.getY(), scale.current()); + final int scaledX1 = scaledX0 + + Scale.scaled(bbox.getWidth(), scale.current()); + final int scaledY1 = scaledY0 + + Scale.scaled(bbox.getHeight(), scale.current()); + + if (x >= scaledX0 && x <= scaledX1 && y >= scaledY0 + && y <= scaledY1) { + return Optional.of(word); + } + } + + return Optional.empty(); + } + + public Optional getPageModel() { + return model; + } + + public void setPageModel(Optional page) { + model = page; + + render(); + } + + public void render() { + delayer.purge(); + + delayer.schedule(new TimerTask() { + @Override + public void run() { + renderer.render(model, scale.current()); + } + }, 200); + } + + @Override + public Component asComponent() { + return this; + } + + @Override + public void freeResources() { + lblOriginal.setIcon(null); + lblRecognition.setIcon(null); + } + + @Override + public Optional getBoxFileModel() { + if (model.isPresent()) { + return Optional.of(model.get().toBoxFileModel()); + } else { + return Optional.empty(); + } + } + + public JLabel getCanvasOriginal() { + return lblOriginal; + } + + public JLabel getCanvasRecognition() { + return lblRecognition; + } + + public JCheckBox getWordBoxes() { + return cbWordBoxes; + } + + public JCheckBox getSymbolBoxes() { + return cbSymbolBoxes; + } + + public JCheckBox getLineNumbers() { + return cbLineNumbers; + } + + public JCheckBox getBaselines() { + return cbBaselines; + } + + public JCheckBox getBlocks() { + return cbBlocks; + } + + public JCheckBox getParagraphs() { + return cbParagraphs; + } + + public void setRenderingFont(String renderingFont) { + renderer.setRenderingFont(renderingFont); + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/SearchField.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java similarity index 91% rename from src/main/java/de/vorb/tesseract/gui/view/SearchField.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java index 61524850..9791c66c 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/SearchField.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SearchField.java @@ -1,12 +1,5 @@ package de.vorb.tesseract.gui.view; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.SystemColor; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -16,6 +9,12 @@ import javax.swing.SwingConstants; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.SystemColor; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; public class SearchField extends JPanel { private static final long serialVersionUID = 1L; @@ -30,11 +29,11 @@ public SearchField() { setBackground(SystemColor.window); setBorder(BorderFactory.createEtchedBorder()); GridBagLayout gridBagLayout = new GridBagLayout(); - gridBagLayout.columnWidths = new int[] { 0, 0, 0, 0 }; - gridBagLayout.rowHeights = new int[] { 0, 0 }; - gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0, - Double.MIN_VALUE }; - gridBagLayout.rowWeights = new double[] { 0.0, Double.MIN_VALUE }; + gridBagLayout.columnWidths = new int[]{0, 0, 0, 0}; + gridBagLayout.rowHeights = new int[]{0, 0}; + gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, + Double.MIN_VALUE}; + gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE}; setLayout(gridBagLayout); lblMagnifier = new JLabel(); diff --git a/src/main/java/de/vorb/tesseract/gui/view/Strokes.java b/gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java similarity index 56% rename from src/main/java/de/vorb/tesseract/gui/view/Strokes.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java index 5c47da30..1c9e6bed 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/Strokes.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/Strokes.java @@ -3,10 +3,10 @@ import java.awt.BasicStroke; import java.awt.Stroke; -public class Strokes { - private Strokes() { - } +public final class Strokes { + + private Strokes() {} public static final Stroke NORMAL = new BasicStroke(1f); - public static final Stroke SELECTION = new BasicStroke(2f); + public static final Stroke SELECTION = new BasicStroke(1f); } diff --git a/src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java similarity index 69% rename from src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java index 65cadb37..27861285 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/GlyphSelectionPane.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolGroupList.java @@ -1,32 +1,33 @@ package de.vorb.tesseract.gui.view; -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.util.Map.Entry; -import java.util.Set; +import de.vorb.tesseract.gui.view.renderer.SymbolGroupListCellRenderer; +import de.vorb.tesseract.util.Symbol; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.util.List; +import java.util.Map.Entry; -import de.vorb.tesseract.gui.view.renderer.GlyphSelectionRenderer; -import de.vorb.tesseract.util.Symbol; - -public class GlyphSelectionPane extends JPanel { +public class SymbolGroupList extends JPanel { private static final long serialVersionUID = 1L; - private final JList>> selectionList; + private final JList>> selectionList; /** * Create the panel. */ - public GlyphSelectionPane() { + public SymbolGroupList() { super(); setLayout(new BorderLayout(0, 0)); JPanel panel = new JPanel(); + panel.setBorder(new EmptyBorder(3, 0, 3, 0)); FlowLayout flowLayout = (FlowLayout) panel.getLayout(); flowLayout.setAlignment(FlowLayout.LEADING); add(panel, BorderLayout.NORTH); @@ -40,11 +41,11 @@ public GlyphSelectionPane() { selectionList = new JList<>(); selectionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - selectionList.setCellRenderer(new GlyphSelectionRenderer()); + selectionList.setCellRenderer(new SymbolGroupListCellRenderer()); scrollPane.setViewportView(selectionList); } - public JList>> getList() { + public JList>> getList() { return selectionList; } } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java new file mode 100644 index 00000000..2d766f73 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolOverview.java @@ -0,0 +1,157 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.event.SymbolLinkListener; +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.view.renderer.SymbolVariantListCellRenderer; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JSplitPane; +import java.awt.BorderLayout; +import java.awt.Component; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; + +public class SymbolOverview extends JPanel implements BoxFileModelComponent { + private static final long serialVersionUID = 1L; + + private final SymbolGroupList glyphSelectionPane; + private final SymbolVariantList glyphListPane; + + private Optional model = Optional.empty(); + private Optional pageModel = Optional.empty(); + + private final LinkedList listeners = new LinkedList<>(); + + public static final Comparator>> SYMBOL_GROUP_COMP = + (o1, o2) -> o2.getValue().size() - o1.getValue().size(); + + public static final Comparator SYMBOL_COMP = (o1, o2) -> { + if (o2.getConfidence() >= o1.getConfidence()) { + return 1; + } + + return -1; + }; + + /** + * Create the panel. + */ + public SymbolOverview() { + super(); + setLayout(new BorderLayout(0, 0)); + + JSplitPane splitPane = new JSplitPane(); + add(splitPane, BorderLayout.CENTER); + + glyphSelectionPane = new SymbolGroupList(); + glyphListPane = new SymbolVariantList(); + + splitPane.setLeftComponent(glyphSelectionPane); + splitPane.setRightComponent(glyphListPane); + } + + public void addSymbolLinkListener(SymbolLinkListener listener) { + listeners.add(listener); + } + + @Override + public Component asComponent() { + return this; + } + + @Override + public Optional getBoxFileModel() { + return model; + } + + public SymbolGroupList getSymbolGroupList() { + return glyphSelectionPane; + } + + public SymbolVariantList getSymbolVariantList() { + return glyphListPane; + } + + @Override + public void setBoxFileModel(Optional model) { + this.model = model; + + if (!model.isPresent()) { + glyphSelectionPane.getList().setModel( + new DefaultListModel<>()); + glyphListPane.getList().setModel(new DefaultListModel<>()); + return; + } + + final JList>> glyphList = + getSymbolGroupList().getList(); + + final HashMap> glyphs = new HashMap<>(); + + final BoxFileModel boxFile = model.get(); + + getSymbolVariantList().getList().setModel( + new DefaultListModel<>()); + + // set a new renderer that has a reference to the thresholded image + getSymbolVariantList().getList().setCellRenderer( + new SymbolVariantListCellRenderer(boxFile.getImage())); + + // insert all symbols into the map + for (final Symbol symbol : boxFile.getBoxes()) { + final String text = symbol.getText(); + + if (!glyphs.containsKey(text)) { + glyphs.put(text, new ArrayList<>()); + } + + glyphs.get(text).add(symbol); + } + + final ArrayList>> entries = new ArrayList<>( + glyphs.entrySet()); + + Collections.sort(entries, SYMBOL_GROUP_COMP); + + final DefaultListModel>> listModel = + new DefaultListModel<>(); + + entries.forEach(listModel::addElement); + + glyphList.setModel(listModel); + } + + @Override + public void setPageModel(Optional model) { + if (model.isPresent()) { + setBoxFileModel(Optional.of(model.get().toBoxFileModel())); + pageModel = model; + } else { + setBoxFileModel(Optional.empty()); + pageModel = model; + } + } + + @Override + public Optional getPageModel() { + return pageModel; + } + + @Override + public void freeResources() { + getSymbolGroupList().getList().setModel( + new DefaultListModel<>()); + getSymbolVariantList().getList().setModel( + new DefaultListModel<>()); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java new file mode 100644 index 00000000..548fb1b3 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/SymbolVariantList.java @@ -0,0 +1,107 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.SymbolOrder; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.Box; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class SymbolVariantList extends JPanel { + private static final long serialVersionUID = 1L; + private final JList glyphList; + private final JComboBox cbOrdering; + private final JMenuItem showInBoxEditor; + private final JMenuItem compareToPrototype; + + /** + * Create the panel. + */ + public SymbolVariantList() { + super(); + setLayout(new BorderLayout(0, 0)); + + JPanel panel = new JPanel(); + FlowLayout fl_panel = (FlowLayout) panel.getLayout(); + fl_panel.setAlignment(FlowLayout.LEADING); + add(panel, BorderLayout.NORTH); + + JLabel lblVariants = new JLabel("Variants"); + panel.add(lblVariants); + + Component horizontalStrut = Box.createHorizontalStrut(20); + panel.add(horizontalStrut); + + JLabel lblOrder = new JLabel("Order by"); + panel.add(lblOrder); + + cbOrdering = new JComboBox<>(); + cbOrdering.setBackground(Color.WHITE); + cbOrdering.setModel(new DefaultComboBoxModel<>( + SymbolOrder.values())); + panel.add(cbOrdering); + + JScrollPane scrollPane = new JScrollPane(); + add(scrollPane, BorderLayout.CENTER); + + glyphList = new JList<>(); + glyphList.setLayoutOrientation(JList.HORIZONTAL_WRAP); + glyphList.setVisibleRowCount(-1); + scrollPane.setViewportView(glyphList); + + final JPopupMenu popupMenu = new JPopupMenu(); + showInBoxEditor = new JMenuItem("Show in Box Editor"); + compareToPrototype = new JMenuItem("Show features"); + popupMenu.add(showInBoxEditor); + popupMenu.add(compareToPrototype); + glyphList.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + showPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + showPopup(e); + } + + private void showPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + final int selection = + glyphList.locationToIndex(e.getPoint()); + + glyphList.setSelectedIndex(selection); + popupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + }); + } + + public JList getList() { + return glyphList; + } + + public JComboBox getOrderingComboBox() { + return cbOrdering; + } + + public JMenuItem getShowInBoxEditor() { + return showInBoxEditor; + } + + public JMenuItem getCompareToPrototype() { + return compareToPrototype; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java b/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java new file mode 100644 index 00000000..6ee2b806 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java @@ -0,0 +1,438 @@ +package de.vorb.tesseract.gui.view; + +import de.vorb.tesseract.gui.model.PageThumbnail; +import de.vorb.tesseract.gui.model.PreferencesUtil; +import de.vorb.tesseract.gui.model.Scale; +import de.vorb.tesseract.gui.util.Filter; +import de.vorb.tesseract.gui.util.Resources; +import de.vorb.tesseract.gui.view.dialogs.PreferencesDialog; +import de.vorb.tesseract.gui.view.i18n.Labels; +import de.vorb.tesseract.gui.view.renderer.PageListCellRenderer; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JSeparator; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Image; +import java.awt.Insets; +import java.awt.SystemColor; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.prefs.Preferences; + +/** + * Swing component that allows to compare the results of Tesseract. + */ +public class TesseractFrame extends JFrame { + private static final long serialVersionUID = 1L; + private final FilteredList listPages; + private final FilteredList listTrainingFiles; + private final PreprocessingPane preprocessingPane; + private final BoxEditor boxEditor; + private final SymbolOverview glyphOverview; + private final RecognitionPane recognitionPane; + private final EvaluationPane evaluationPane; + + private final JLabel lblScaleFactor; + private final JProgressBar pbLoadPage; + private final JSplitPane spMain; + private final JMenuItem mnNewProject; + private final JTabbedPane tabsMain; + private final JMenuItem mnPreferences; + private final JMenuItem mnBatchExport; + private final JSeparator separator_1; + private final JMenuItem mnOpenProjectDirectory; + private final JMenuItem mnOpenBoxFile; + private final JMenu mnEdit; + + private final JSeparator separator_2; + private final JMenuItem mnCloseProject; + private final JMenuItem mnSaveBoxFile; + private final JMenuItem mnImportTranscriptions; + + private final JMenu mnTools; + private final JMenuItem mnCharacterHistogram; + private final JMenuItem mnInspectUnicharset; + + private final JMenuItem mnExit; + private final JMenuItem mnTesseractTrainer; + + private final Scale scale; + + /** + * Create the application. + */ + public TesseractFrame() { + super(); + final Toolkit t = Toolkit.getDefaultToolkit(); + + // load and set multiple icon sizes + final List appIcons = new LinkedList<>(); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_16.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_96.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_256.png"))); + setIconImages(appIcons); + + setLocationByPlatform(true); + setMinimumSize(new Dimension(1100, 680)); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + scale = new Scale(); + preprocessingPane = new PreprocessingPane(); + boxEditor = new BoxEditor(scale); + glyphOverview = new SymbolOverview(); + + final Preferences prefs = PreferencesUtil.getPreferences(); + final String renderingFont = prefs.get(PreferencesDialog.KEY_RENDERING_FONT, Font.SANS_SERIF); + final String editorFont = prefs.get(PreferencesDialog.KEY_EDITOR_FONT, Font.MONOSPACED); + + recognitionPane = new RecognitionPane(scale, renderingFont); + evaluationPane = new EvaluationPane(scale, editorFont); + evaluationPane.getGenerateReportButton().setIcon( + new ImageIcon(TesseractFrame.class.getResource("/icons/report.png"))); + pbLoadPage = new JProgressBar(); + spMain = new JSplitPane(); + + listPages = new FilteredList<>( + query -> { + final String[] terms = + query.toLowerCase().split("\\s+"); + + final Filter filter; + if (query.isEmpty()) { + filter = null; + } else { + // item must contain all terms in query + filter = item -> { + String filename = + item.getFile().getFileName().toString().toLowerCase(); + for (String term : terms) { + if (!filename.contains(term)) { + return false; + } + } + return true; + }; + } + return Optional.ofNullable(filter); + }); + listPages.getList().setCellRenderer(new PageListCellRenderer()); + + listPages.setMinimumSize(new Dimension(250, 100)); + listPages.getList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + listPages.setBorder(BorderFactory.createTitledBorder("Image")); + + // filtered string list + listTrainingFiles = + new FilteredList<>(query -> { + final String[] terms = + query.toLowerCase().split("\\s+"); + + final Filter filter; + if (query.isEmpty()) { + filter = null; + } else { + // item must contain all terms in query + filter = item -> { + for (String term : terms) { + if (!item.toLowerCase().contains(term)) { + return false; + } + } + return true; + }; + } + return Optional.ofNullable(filter); + }); + + listTrainingFiles.setBorder(BorderFactory.createTitledBorder("Traineddata File")); + + setTitle(Labels.getLabel(getLocale(), "frame_title")); + + // Menu + + final JMenuBar menuBar = new JMenuBar(); + setJMenuBar(menuBar); + + final JMenu mnFile = new JMenu( + Labels.getLabel(getLocale(), "menu_file")); + menuBar.add(mnFile); + + mnNewProject = new JMenuItem("New Project"); + mnNewProject.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, + InputEvent.CTRL_MASK)); + mnFile.add(mnNewProject); + + mnOpenBoxFile = new JMenuItem("Open Box File..."); + mnOpenBoxFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, + InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)); + mnFile.add(mnOpenBoxFile); + + separator_2 = new JSeparator(); + mnFile.add(separator_2); + + mnSaveBoxFile = new JMenuItem("Save Box File"); + mnSaveBoxFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, + InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)); + mnSaveBoxFile.setEnabled(false); + mnSaveBoxFile.setIcon(new ImageIcon( + TesseractFrame.class.getResource("/icons/table_save.png"))); + mnFile.add(mnSaveBoxFile); + + mnOpenProjectDirectory = new JMenuItem("Open Project Directory"); + mnOpenProjectDirectory.setEnabled(false); + mnOpenProjectDirectory.setIcon(new ImageIcon( + TesseractFrame.class.getResource("/icons/folder_explore.png"))); + mnFile.add(mnOpenProjectDirectory); + + mnCloseProject = new JMenuItem("Close Project"); + mnCloseProject.setEnabled(false); + mnFile.add(mnCloseProject); + + separator_1 = new JSeparator(); + mnFile.add(separator_1); + + mnImportTranscriptions = new JMenuItem("Import Transcriptions..."); + mnImportTranscriptions.setEnabled(false); + mnImportTranscriptions.setAccelerator(KeyStroke.getKeyStroke( + KeyEvent.VK_I, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)); + mnFile.add(mnImportTranscriptions); + + mnBatchExport = new JMenuItem("Batch Export..."); + mnBatchExport.setEnabled(false); + mnBatchExport.setIcon(new ImageIcon( + TesseractFrame.class.getResource("/icons/book_next.png"))); + mnBatchExport.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, + InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)); + mnFile.add(mnBatchExport); + + final JSeparator separator = new JSeparator(); + mnFile.add(separator); + + mnExit = new JMenuItem("Exit"); + mnExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, + InputEvent.CTRL_MASK)); + mnFile.add(mnExit); + + mnEdit = new JMenu("Edit"); + menuBar.add(mnEdit); + + mnPreferences = new JMenuItem("Preferences"); + mnPreferences.setIcon(new ImageIcon( + TesseractFrame.class.getResource("/icons/cog.png"))); + mnEdit.add(mnPreferences); + + mnTools = new JMenu("Tools"); + menuBar.add(mnTools); + + mnCharacterHistogram = new JMenuItem("Character Histogram..."); + mnTools.add(mnCharacterHistogram); + + mnInspectUnicharset = new JMenuItem("Debug Unicharset..."); + mnTools.add(mnInspectUnicharset); + + mnTesseractTrainer = new JMenuItem("Tesseract Trainer..."); + mnTesseractTrainer.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, + InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK)); + mnTools.add(mnTesseractTrainer); + + final JMenu mnHelp = new JMenu( + Labels.getLabel(getLocale(), "menu_help")); + menuBar.add(mnHelp); + + final JMenuItem mntmAbout = new JMenuItem(Labels.getLabel(getLocale(), + "menu_about")); + mntmAbout.setIcon(new ImageIcon( + TesseractFrame.class.getResource("/icons/information.png"))); + mntmAbout.addActionListener(e -> JOptionPane.showMessageDialog(TesseractFrame.this, + Labels.getLabel(getLocale(), "about_message"), + Labels.getLabel(getLocale(), "about_title"), + JOptionPane.INFORMATION_MESSAGE)); + mnHelp.add(mntmAbout); + + // Contents + + final JPanel panel = new JPanel(); + panel.setBackground(SystemColor.menu); + panel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(panel, BorderLayout.SOUTH); + final GridBagLayout gbl_panel = new GridBagLayout(); + gbl_panel.columnWidths = new int[]{0, 50, 417, + 0, 0}; + gbl_panel.rowHeights = new int[]{14, 0}; + gbl_panel.columnWeights = new double[]{0.0, 0.0, 1.0, 0.0, + Double.MIN_VALUE}; + gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + panel.setLayout(gbl_panel); + + JLabel lblScale = new JLabel("Scale:"); + GridBagConstraints gbc_lblScale = new GridBagConstraints(); + gbc_lblScale.insets = new Insets(0, 0, 0, 5); + gbc_lblScale.gridx = 0; + gbc_lblScale.gridy = 0; + panel.add(lblScale, gbc_lblScale); + + lblScaleFactor = new JLabel(scale.toString()); + GridBagConstraints gbc_lblScaleFactor = new GridBagConstraints(); + gbc_lblScaleFactor.anchor = GridBagConstraints.WEST; + gbc_lblScaleFactor.insets = new Insets(0, 0, 0, 5); + gbc_lblScaleFactor.gridx = 1; + gbc_lblScaleFactor.gridy = 0; + panel.add(lblScaleFactor, gbc_lblScaleFactor); + + GridBagConstraints gbc_pbRegognitionProgress = new GridBagConstraints(); + gbc_pbRegognitionProgress.gridx = 3; + gbc_pbRegognitionProgress.gridy = 0; + panel.add(pbLoadPage, gbc_pbRegognitionProgress); + getContentPane().add(spMain, BorderLayout.CENTER); + + tabsMain = new JTabbedPane(); + tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_preprocessing"), + Resources.getIcon("contrast"), preprocessingPane); + + tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_boxeditor"), + Resources.getIcon("table_edit"), boxEditor); + + tabsMain.addTab( + Labels.getLabel(getLocale(), "tab_main_symboloverview"), + Resources.getIcon("application_view_icons"), + glyphOverview); + + tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_recognition"), + Resources.getIcon("application_tile_horizontal"), + recognitionPane); + + tabsMain.addTab(Labels.getLabel(getLocale(), "tab_main_evaluation"), + Resources.getIcon("chart_pie"), + evaluationPane); + + spMain.setRightComponent(tabsMain); + + JSplitPane splitPane = new JSplitPane(); + splitPane.setResizeWeight(1.0); + splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); + spMain.setLeftComponent(splitPane); + splitPane.setLeftComponent(listPages); + splitPane.setRightComponent(listTrainingFiles); + } + + public MainComponent getActiveComponent() { + return (MainComponent) tabsMain.getSelectedComponent(); + } + + public BoxEditor getBoxEditor() { + return boxEditor; + } + + public JTabbedPane getMainTabs() { + return tabsMain; + } + + public JMenuItem getMenuItemNewProject() { + return mnNewProject; + } + + public JMenuItem getMenuItemOpenBoxFile() { + return mnOpenBoxFile; + } + + public JMenuItem getMenuItemSaveBoxFile() { + return mnSaveBoxFile; + } + + public JMenuItem getMenuItemCloseProject() { + return mnCloseProject; + } + + public JMenuItem getMenuItemOpenProjectDirectory() { + return mnOpenProjectDirectory; + } + + public JMenuItem getMenuItemImportTranscriptions() { + return mnImportTranscriptions; + } + + public JMenuItem getMenuItemBatchExport() { + return mnBatchExport; + } + + public JMenuItem getMenuItemExit() { + return mnExit; + } + + public JMenuItem getMenuItemPreferences() { + return mnPreferences; + } + + public JMenuItem getMenuItemCharacterHistogram() { + return mnCharacterHistogram; + } + + public JMenuItem getMenuItemInspectUnicharset() { + return mnInspectUnicharset; + } + + public JMenuItem getMenuItemTesseractTrainer() { + return mnTesseractTrainer; + } + + public FilteredList getPages() { + return listPages; + } + + public PreprocessingPane getPreprocessingPane() { + return preprocessingPane; + } + + public JProgressBar getProgressBar() { + return pbLoadPage; + } + + public RecognitionPane getRecognitionPane() { + return recognitionPane; + } + + public Scale getScale() { + return scale; + } + + public JLabel getScaleLabel() { + return lblScaleFactor; + } + + public SymbolOverview getSymbolOverview() { + return glyphOverview; + } + + public EvaluationPane getEvaluationPane() { + return evaluationPane; + } + + public FilteredList getTraineddataFiles() { + return listTrainingFiles; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java new file mode 100644 index 00000000..10a31d66 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportDialog.java @@ -0,0 +1,318 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import de.vorb.tesseract.gui.controller.TesseractController; +import de.vorb.tesseract.gui.model.BatchExportModel; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; +import javax.swing.UIManager; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +public class BatchExportDialog extends JDialog implements ActionListener { + private static final long serialVersionUID = 1L; + + private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors(); + private static final int MIN_THREADS = 1; + private static final int DEFAULT_THREADS = MAX_THREADS - 1; + + private final JButton btnExport; + private final JCheckBox chckbxTxt; + private final JCheckBox chckbxHtml; + private final JSpinner spinnerWorkerThreads; + private final JPanel panel_2; + private final JPanel panel_3; + private final JTextField tfDestinationDir; + private final JButton btnDestinationDir; + private final JLabel lblDestinationDir; + private final JLabel lblFileFormats; + private final JButton btnCancel; + private final JCheckBox chckbxOpenDestination; + + private final TesseractController controller; + + private BatchExportModel exportModel = null; + private JCheckBox chckbxExportImages; + private JLabel lblExport; + private JCheckBox chckbxXml; + private JCheckBox chckbxEvaluationReports; + + /** + * Create the panel. + */ + public BatchExportDialog(TesseractController controller) { + setIconImage(Toolkit.getDefaultToolkit().getImage( + BatchExportDialog.class.getResource("/icons/book_next.png"))); + setLocationRelativeTo(controller.getView()); + + this.controller = controller; + + setResizable(false); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + + setTitle("Batch Export"); + getContentPane().setLayout(new BorderLayout(0, 0)); + + setModalityType(ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(); + FlowLayout flowLayout = (FlowLayout) panel.getLayout(); + flowLayout.setAlignment(FlowLayout.TRAILING); + getContentPane().add(panel, BorderLayout.SOUTH); + + btnExport = new JButton("Export"); + btnExport.addActionListener(this); + + chckbxOpenDestination = new JCheckBox("Open Destination"); + panel.add(chckbxOpenDestination); + panel.add(btnExport); + + btnCancel = new JButton("Cancel"); + btnCancel.addActionListener(this); + panel.add(btnCancel); + + JPanel panel_1 = new JPanel(); + panel_1.setBorder(new EmptyBorder(4, 4, 4, 4)); + getContentPane().add(panel_1, BorderLayout.CENTER); + panel_1.setLayout(new BoxLayout(panel_1, BoxLayout.Y_AXIS)); + + panel_3 = new JPanel(); + panel_3.setBorder(new CompoundBorder(new TitledBorder( + UIManager.getBorder("TitledBorder.border"), "Export Options", + TitledBorder.LEADING, TitledBorder.TOP, null, + new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4))); + panel_1.add(panel_3); + GridBagLayout gbl_panel_3 = new GridBagLayout(); + gbl_panel_3.columnWidths = new int[]{0, 0, 0, 0}; + gbl_panel_3.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0}; + gbl_panel_3.columnWeights = new double[]{0.0, 1.0, 0.0, + Double.MIN_VALUE}; + gbl_panel_3.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, + Double.MIN_VALUE}; + panel_3.setLayout(gbl_panel_3); + + lblDestinationDir = new JLabel("Destination Directory"); + GridBagConstraints gbc_lblDestinationDir = new GridBagConstraints(); + gbc_lblDestinationDir.insets = new Insets(0, 0, 5, 5); + gbc_lblDestinationDir.anchor = GridBagConstraints.EAST; + gbc_lblDestinationDir.gridx = 0; + gbc_lblDestinationDir.gridy = 0; + panel_3.add(lblDestinationDir, gbc_lblDestinationDir); + + tfDestinationDir = new JTextField(); + GridBagConstraints gbc_tfDestinationDir = new GridBagConstraints(); + gbc_tfDestinationDir.insets = new Insets(0, 0, 5, 5); + gbc_tfDestinationDir.fill = GridBagConstraints.HORIZONTAL; + gbc_tfDestinationDir.gridx = 1; + gbc_tfDestinationDir.gridy = 0; + panel_3.add(tfDestinationDir, gbc_tfDestinationDir); + tfDestinationDir.setColumns(10); + + btnDestinationDir = new JButton("..."); + GridBagConstraints gbc_btnDestinationDir = new GridBagConstraints(); + gbc_btnDestinationDir.insets = new Insets(0, 0, 5, 0); + gbc_btnDestinationDir.gridx = 2; + gbc_btnDestinationDir.gridy = 0; + panel_3.add(btnDestinationDir, gbc_btnDestinationDir); + btnDestinationDir.addActionListener(this); + + lblFileFormats = new JLabel("File Formats"); + GridBagConstraints gbc_lblFileFormats = new GridBagConstraints(); + gbc_lblFileFormats.anchor = GridBagConstraints.EAST; + gbc_lblFileFormats.insets = new Insets(0, 0, 5, 5); + gbc_lblFileFormats.gridx = 0; + gbc_lblFileFormats.gridy = 1; + panel_3.add(lblFileFormats, gbc_lblFileFormats); + + chckbxTxt = new JCheckBox("TXT"); + GridBagConstraints gbc_chckbxText = new GridBagConstraints(); + gbc_chckbxText.anchor = GridBagConstraints.WEST; + gbc_chckbxText.insets = new Insets(0, 0, 5, 5); + gbc_chckbxText.gridx = 1; + gbc_chckbxText.gridy = 1; + panel_3.add(chckbxTxt, gbc_chckbxText); + chckbxTxt.setSelected(true); + + chckbxXml = new JCheckBox("XML"); + GridBagConstraints gbc_chckbxXml = new GridBagConstraints(); + gbc_chckbxXml.anchor = GridBagConstraints.WEST; + gbc_chckbxXml.insets = new Insets(0, 0, 5, 5); + gbc_chckbxXml.gridx = 1; + gbc_chckbxXml.gridy = 2; + panel_3.add(chckbxXml, gbc_chckbxXml); + + chckbxHtml = new JCheckBox("HTML"); + chckbxHtml.setVisible(false); + GridBagConstraints gbc_chckbxHtml = new GridBagConstraints(); + gbc_chckbxHtml.anchor = GridBagConstraints.WEST; + gbc_chckbxHtml.insets = new Insets(0, 0, 5, 5); + gbc_chckbxHtml.gridx = 1; + gbc_chckbxHtml.gridy = 3; + panel_3.add(chckbxHtml, gbc_chckbxHtml); + + lblExport = new JLabel("Export"); + GridBagConstraints gbc_lblExport = new GridBagConstraints(); + gbc_lblExport.anchor = GridBagConstraints.EAST; + gbc_lblExport.insets = new Insets(0, 0, 5, 5); + gbc_lblExport.gridx = 0; + gbc_lblExport.gridy = 4; + panel_3.add(lblExport, gbc_lblExport); + + chckbxExportImages = new JCheckBox("Preprocessed Images"); + chckbxExportImages.setSelected(true); + GridBagConstraints gbc_chckbxExportImages = new GridBagConstraints(); + gbc_chckbxExportImages.insets = new Insets(0, 0, 5, 5); + gbc_chckbxExportImages.gridx = 1; + gbc_chckbxExportImages.gridy = 4; + panel_3.add(chckbxExportImages, gbc_chckbxExportImages); + + chckbxEvaluationReports = new JCheckBox("Evaluation Reports"); + chckbxEvaluationReports.setSelected(true); + GridBagConstraints gbc_chckbxEvaluationReports = new GridBagConstraints(); + gbc_chckbxEvaluationReports.anchor = GridBagConstraints.WEST; + gbc_chckbxEvaluationReports.insets = new Insets(0, 0, 0, 5); + gbc_chckbxEvaluationReports.gridx = 1; + gbc_chckbxEvaluationReports.gridy = 5; + panel_3.add(chckbxEvaluationReports, gbc_chckbxEvaluationReports); + + panel_2 = new JPanel(); + panel_2.setBorder(new CompoundBorder(new TitledBorder( + UIManager.getBorder("TitledBorder.border"), + "Advanced Settings", TitledBorder.LEADING, TitledBorder.TOP, + null, new Color(0, 0, 0)), new EmptyBorder(4, 4, 4, 4))); + panel_1.add(panel_2); + GridBagLayout gbl_panel_2 = new GridBagLayout(); + gbl_panel_2.columnWidths = new int[]{67, 44, 0}; + gbl_panel_2.rowHeights = new int[]{20, 0}; + gbl_panel_2.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + gbl_panel_2.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + panel_2.setLayout(gbl_panel_2); + + JLabel lblWorkerThreads = new JLabel("Number of Worker Threads"); + GridBagConstraints gbc_lblWorkerThreads = new GridBagConstraints(); + gbc_lblWorkerThreads.anchor = GridBagConstraints.WEST; + gbc_lblWorkerThreads.insets = new Insets(0, 0, 0, 5); + gbc_lblWorkerThreads.gridx = 0; + gbc_lblWorkerThreads.gridy = 0; + panel_2.add(lblWorkerThreads, gbc_lblWorkerThreads); + + spinnerWorkerThreads = new JSpinner(); + GridBagConstraints gbc_spinnerWorkerThreads = new GridBagConstraints(); + gbc_spinnerWorkerThreads.fill = GridBagConstraints.HORIZONTAL; + gbc_spinnerWorkerThreads.anchor = GridBagConstraints.NORTH; + gbc_spinnerWorkerThreads.gridx = 1; + gbc_spinnerWorkerThreads.gridy = 0; + panel_2.add(spinnerWorkerThreads, gbc_spinnerWorkerThreads); + spinnerWorkerThreads.setModel(new SpinnerNumberModel(DEFAULT_THREADS, + MIN_THREADS, MAX_THREADS, 1)); + + pack(); + setMinimumSize(getSize()); + setSize(new Dimension(276, 280)); + + setLocationRelativeTo(controller.getView()); + } + + public JButton getExportButton() { + return btnExport; + } + + public JCheckBox getTextCheckBox() { + return chckbxTxt; + } + + public JCheckBox getHTMLCheckBox() { + return chckbxHtml; + } + + public JSpinner getWorkerThreadsSpinner() { + return spinnerWorkerThreads; + } + + public JTextField getDestinationDirectoryTextField() { + return tfDestinationDir; + } + + public static Optional showBatchExportDialog( + TesseractController controller) { + final BatchExportDialog dialog = new BatchExportDialog(controller); + dialog.setVisible(true); + + return Optional.ofNullable(dialog.exportModel); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (evt.getSource() == btnCancel) { + this.dispose(); + } else if (evt.getSource() == btnDestinationDir) { + final JFileChooser dirChooser = new JFileChooser( + controller.getProjectModel().get().getProjectDir().toFile()); + dirChooser.setDialogTitle("Choose Destination Directory"); + dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + final int result = dirChooser.showOpenDialog(controller.getView()); + if (result == JFileChooser.APPROVE_OPTION) { + final File destinationDir = dirChooser.getSelectedFile(); + tfDestinationDir.setText(destinationDir.toString()); + } + } else if (evt.getSource() == btnExport) { + try { + if (tfDestinationDir.getText().equals("")) { + throw new InvalidPathException("", "empty"); + } + + final Path destinationDir = + Paths.get(tfDestinationDir.getText()); + + if (!Files.exists(destinationDir)) { + throw new InvalidPathException(destinationDir.toString(), + "doesn't exist"); + } + + final boolean exportTXT = chckbxTxt.isSelected(); + final boolean exportHTML = chckbxHtml.isSelected(); + final boolean exportXML = chckbxXml.isSelected(); + final boolean exportImages = chckbxExportImages.isSelected(); + final boolean exportReports = chckbxEvaluationReports.isSelected(); + final boolean openDestination = chckbxOpenDestination.isSelected(); + final int numThreads = (Integer) spinnerWorkerThreads.getValue(); + + exportModel = new BatchExportModel( + destinationDir, exportTXT, exportHTML, exportXML, + exportImages, exportReports, numThreads, + openDestination); + + this.setVisible(false); + } catch (InvalidPathException e) { + Dialogs.showError(this, "Invalid Destination Directory", + "The given destination directory doesn't exist."); + } + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java new file mode 100644 index 00000000..8d106d04 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/BatchExportProgressDialog.java @@ -0,0 +1,95 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Toolkit; + +public class BatchExportProgressDialog extends JDialog { + private static final long serialVersionUID = 1L; + + private final JPanel contentPanel = new JPanel(); + + private final JLabel lblFilename; + private final JProgressBar progressBar; + private final JButton btnCancel; + + /** + * Create the dialog. + */ + public BatchExportProgressDialog() { + setIconImage(Toolkit.getDefaultToolkit().getImage( + BatchExportProgressDialog.class.getResource("/icons/book_next.png"))); + setModalityType(ModalityType.APPLICATION_MODAL); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setResizable(false); + setTitle("Batch Export Progress"); + setBounds(100, 100, 450, 130); + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(20, 20, 20, 20)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + GridBagLayout gbl_contentPanel = new GridBagLayout(); + gbl_contentPanel.columnWidths = new int[]{0, 0, 0}; + gbl_contentPanel.rowHeights = new int[]{0, 20, 0, 0}; + gbl_contentPanel.columnWeights = new double[]{0.0, 1.0, + Double.MIN_VALUE}; + gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, 0.0, + Double.MIN_VALUE}; + contentPanel.setLayout(gbl_contentPanel); + { + JLabel lblProcessing = new JLabel("Processing"); + GridBagConstraints gbc_lblCurrentFile = new GridBagConstraints(); + gbc_lblCurrentFile.anchor = GridBagConstraints.WEST; + gbc_lblCurrentFile.insets = new Insets(0, 0, 5, 5); + gbc_lblCurrentFile.gridx = 0; + gbc_lblCurrentFile.gridy = 0; + contentPanel.add(lblProcessing, gbc_lblCurrentFile); + } + { + lblFilename = new JLabel("Filename"); + GridBagConstraints gbc_lblFilename = new GridBagConstraints(); + gbc_lblFilename.anchor = GridBagConstraints.WEST; + gbc_lblFilename.insets = new Insets(0, 0, 5, 0); + gbc_lblFilename.gridx = 1; + gbc_lblFilename.gridy = 0; + contentPanel.add(lblFilename, gbc_lblFilename); + } + { + progressBar = new JProgressBar(); + progressBar.setStringPainted(true); + GridBagConstraints gbc_progressBar = new GridBagConstraints(); + gbc_progressBar.insets = new Insets(0, 0, 5, 0); + gbc_progressBar.gridwidth = 2; + gbc_progressBar.fill = GridBagConstraints.BOTH; + gbc_progressBar.gridx = 0; + gbc_progressBar.gridy = 1; + contentPanel.add(progressBar, gbc_progressBar); + } + { + btnCancel = new JButton("Cancel"); + GridBagConstraints gbc_btnNewButton = new GridBagConstraints(); + gbc_btnNewButton.anchor = GridBagConstraints.EAST; + gbc_btnNewButton.gridx = 1; + gbc_btnNewButton.gridy = 2; + contentPanel.add(btnCancel, gbc_btnNewButton); + + // dispose of dialog on cancel + btnCancel.addActionListener(e -> BatchExportProgressDialog.this.dispose()); + } + } + + public JLabel getFileNameLabel() { + return lblFilename; + } + + public JProgressBar getProgressBar() { + return progressBar; + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java similarity index 88% rename from src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java index 09abb810..7d2b3703 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/CharTableDialog.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharTableDialog.java @@ -1,6 +1,6 @@ -package de.vorb.tesseract.gui.view; +package de.vorb.tesseract.gui.view.dialogs; -import java.awt.BorderLayout; +import de.vorb.tesseract.gui.model.CharTableModel; import javax.swing.JButton; import javax.swing.JDialog; @@ -9,8 +9,7 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; - -import de.vorb.tesseract.gui.model.CharTableModel; +import java.awt.BorderLayout; public class CharTableDialog extends JDialog { private static final long serialVersionUID = 1L; @@ -23,6 +22,7 @@ public class CharTableDialog extends JDialog { */ public CharTableDialog() { super(); + setTitle("Special characters"); setAlwaysOnTop(true); setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); @@ -37,7 +37,7 @@ public CharTableDialog() { JPanel panel_1 = new JPanel(); panel.add(panel_1, BorderLayout.WEST); - JLabel label = new JLabel("Codepoint"); + JLabel label = new JLabel("Code point"); panel_1.add(label); textField = new JFormattedTextField(); @@ -61,9 +61,4 @@ public CharTableDialog() { table.getColumnModel().getColumn(1).setPreferredWidth(300); scrollPane.setViewportView(table); } - - public static void main(String[] args) { - final JDialog d = new CharTableDialog(); - d.setVisible(true); - } } diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java new file mode 100644 index 00000000..414056d6 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/CharacterHistogram.java @@ -0,0 +1,77 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import de.vorb.tesseract.gui.view.TesseractFrame; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.EmptyBorder; +import javax.swing.table.DefaultTableModel; +import java.awt.BorderLayout; +import java.awt.Image; +import java.awt.Toolkit; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class CharacterHistogram extends JFrame { + private static final long serialVersionUID = 1L; + + private JPanel contentPane; + private JTable tabHistogram; + + /** + * Create the frame. + * + * @param histogram + */ + public CharacterHistogram(Map histogram) { + final Toolkit t = Toolkit.getDefaultToolkit(); + final List appIcons = new LinkedList<>(); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_16.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_96.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_256.png"))); + setIconImages(appIcons); + + setTitle("Character Histogram"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setBounds(100, 100, 450, 300); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + contentPane.setLayout(new BorderLayout(0, 0)); + setContentPane(contentPane); + + JScrollPane scrollPane = new JScrollPane(); + contentPane.add(scrollPane, BorderLayout.CENTER); + + tabHistogram = new JTable(); + tabHistogram.setFillsViewportHeight(true); + tabHistogram.setRowSelectionAllowed(false); + + final Set> entries = histogram.entrySet(); + final Object[][] tableData = new Object[entries.size()][2]; + int i = 0; + + for (Entry entry : entries) { + tableData[i][0] = entry.getKey(); + tableData[i++][1] = entry.getValue(); + } + + tabHistogram.setModel(new DefaultTableModel( + tableData, + new String[]{ + "Character", "Count" + } + )); + scrollPane.setViewportView(tabHistogram); + + setMinimumSize(getSize()); + } + +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java new file mode 100644 index 00000000..8f88bb42 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/Dialogs.java @@ -0,0 +1,34 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import javax.swing.JOptionPane; +import java.awt.Component; + +public final class Dialogs { + + private Dialogs() {} + + public static void showInfo(Component parent, String title, + String message) { + JOptionPane.showMessageDialog(parent, message, title, + JOptionPane.INFORMATION_MESSAGE); + } + + public static void showWarning(Component parent, String title, + String message) { + JOptionPane.showMessageDialog(parent, message, title, + JOptionPane.WARNING_MESSAGE); + } + + public static void showError(Component parent, String title, + String message) { + JOptionPane.showMessageDialog(parent, message, title, + JOptionPane.ERROR_MESSAGE); + } + + public static boolean ask(Component parent, String title, String question) { + int result = JOptionPane.showConfirmDialog(parent, question, title, + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); + + return result == JOptionPane.OK_OPTION; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java new file mode 100644 index 00000000..52a4a404 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/ImportTranscriptionDialog.java @@ -0,0 +1,202 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.filechooser.FileFilter; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ImportTranscriptionDialog extends JDialog { + private static final long serialVersionUID = 1L; + + private final JPanel contentPanel = new JPanel(); + private JTextField tfFile; + private JTextField tfPageSeparator; + + private boolean approved = false; + + /** + * Create the dialog. + */ + public ImportTranscriptionDialog() { + setTitle("Import Transcription"); + setBounds(100, 100, 450, 176); + setModalityType(ModalityType.APPLICATION_MODAL); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + GridBagLayout gbl_contentPanel = new GridBagLayout(); + gbl_contentPanel.columnWidths = new int[]{0, 0, 0, 0}; + gbl_contentPanel.rowHeights = new int[]{0, 0, 0, 0}; + gbl_contentPanel.columnWeights = new double[]{0.0, 1.0, 0.0, + Double.MIN_VALUE}; + gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, 0.0, + Double.MIN_VALUE}; + contentPanel.setLayout(gbl_contentPanel); + { + JLabel lblTranscriptionFile = new JLabel("Transcription File"); + GridBagConstraints gbc_lblTranscriptionFile = new GridBagConstraints(); + gbc_lblTranscriptionFile.insets = new Insets(0, 0, 5, 5); + gbc_lblTranscriptionFile.anchor = GridBagConstraints.EAST; + gbc_lblTranscriptionFile.gridx = 0; + gbc_lblTranscriptionFile.gridy = 0; + contentPanel.add(lblTranscriptionFile, gbc_lblTranscriptionFile); + } + { + tfFile = new JTextField(); + GridBagConstraints gbc_tfFile = new GridBagConstraints(); + gbc_tfFile.insets = new Insets(0, 0, 5, 5); + gbc_tfFile.fill = GridBagConstraints.HORIZONTAL; + gbc_tfFile.gridx = 1; + gbc_tfFile.gridy = 0; + contentPanel.add(tfFile, gbc_tfFile); + tfFile.setColumns(10); + } + { + JButton btnSelect = new JButton("Select..."); + btnSelect.addActionListener(evt -> { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Plain text (*.txt)"; + } + + @Override + public boolean accept(File f) { + return f.canRead() && (f.isDirectory() || + f.getName().endsWith(".txt")); + } + }); + + final int result = fc.showOpenDialog( + ImportTranscriptionDialog.this); + if (result == JFileChooser.APPROVE_OPTION) { + tfFile.setText(fc.getSelectedFile().toString()); + } + }); + GridBagConstraints gbc_btnSelect = new GridBagConstraints(); + gbc_btnSelect.insets = new Insets(0, 0, 5, 0); + gbc_btnSelect.gridx = 2; + gbc_btnSelect.gridy = 0; + contentPanel.add(btnSelect, gbc_btnSelect); + } + { + JLabel lblPageSeparator = new JLabel("Page Separator"); + GridBagConstraints gbc_lblPageSeparator = new GridBagConstraints(); + gbc_lblPageSeparator.insets = new Insets(0, 0, 5, 5); + gbc_lblPageSeparator.anchor = GridBagConstraints.EAST; + gbc_lblPageSeparator.gridx = 0; + gbc_lblPageSeparator.gridy = 1; + contentPanel.add(lblPageSeparator, gbc_lblPageSeparator); + } + { + tfPageSeparator = new JTextField("~~~~~"); + GridBagConstraints gbc_textField_1 = new GridBagConstraints(); + gbc_textField_1.insets = new Insets(0, 0, 5, 5); + gbc_textField_1.fill = GridBagConstraints.HORIZONTAL; + gbc_textField_1.gridx = 1; + gbc_textField_1.gridy = 1; + contentPanel.add(tfPageSeparator, gbc_textField_1); + tfPageSeparator.setColumns(10); + } + { + JLabel lblWarning = new JLabel("Warning:"); + lblWarning.setFont(new Font("Tahoma", Font.BOLD, 11)); + GridBagConstraints gbc_lblWarning = new GridBagConstraints(); + gbc_lblWarning.anchor = GridBagConstraints.EAST; + gbc_lblWarning.insets = new Insets(0, 0, 0, 5); + gbc_lblWarning.gridx = 0; + gbc_lblWarning.gridy = 2; + contentPanel.add(lblWarning, gbc_lblWarning); + } + { + JLabel lblExistingTranscriptionFiles = new JLabel( + "Existing transcription files will be lost!"); + GridBagConstraints gbc_lblExistingTranscriptionFiles = new GridBagConstraints(); + gbc_lblExistingTranscriptionFiles.anchor = GridBagConstraints.WEST; + gbc_lblExistingTranscriptionFiles.insets = new Insets(0, 0, 0, 5); + gbc_lblExistingTranscriptionFiles.gridx = 1; + gbc_lblExistingTranscriptionFiles.gridy = 2; + contentPanel.add(lblExistingTranscriptionFiles, + gbc_lblExistingTranscriptionFiles); + } + { + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + { + JButton okButton = new JButton("Import"); + okButton.addActionListener(evt -> { + if (getTranscriptionFile() != null) { + if (getPageSeparator().isEmpty()) { + Dialogs.showWarning( + ImportTranscriptionDialog.this, + "Empty Separator", + "The separator must not be empty."); + return; + } + + approved = true; + + ImportTranscriptionDialog.this.dispose(); + } else { + Dialogs.showWarning(ImportTranscriptionDialog.this, + "Invalid File", + "The given transcription file is invalid."); + } + }); + + buttonPane.add(okButton); + getRootPane().setDefaultButton(okButton); + } + { + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(evt -> { + approved = false; + + ImportTranscriptionDialog.this.dispose(); + }); + buttonPane.add(cancelButton); + } + } + + } + + public boolean isApproved() { + return approved; + } + + public Path getTranscriptionFile() { + try { + final Path file = Paths.get(tfFile.getText()); + if (Files.isRegularFile(file)) { + return file; + } + } catch (InvalidPathException e) { + } + + return null; + } + + public String getPageSeparator() { + return tfPageSeparator.getText(); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java new file mode 100644 index 00000000..6bcdbf69 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/NewProjectDialog.java @@ -0,0 +1,289 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import de.vorb.tesseract.gui.model.ProjectModel; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.MatteBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.SystemColor; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.prefs.Preferences; + +public class NewProjectDialog extends JDialog implements ActionListener, + DocumentListener { + private static final long serialVersionUID = 1L; + + private static final String PREF_DIR = "dir"; + + private final JTextField tfPath; + private final JButton btnPathSelect; + + private final JCheckBox cbTiff; + private final JCheckBox cbPng; + private final JCheckBox cbJpeg; + + private final JButton btnCreate; + private final JButton btnCancel; + + private Optional result = Optional.empty(); + + /** + * Create the dialog. + * + * @param owner + */ + private NewProjectDialog(final Window owner) { + super(owner); + + setModalityType(ModalityType.APPLICATION_MODAL); + + setIconImage(Toolkit.getDefaultToolkit().getImage( + NewProjectDialog.class.getResource("/logos/logo_16.png"))); + setTitle("New Project"); + + JPanel panel = new JPanel(); + getContentPane().add(panel); + panel.setLayout(new BorderLayout(0, 0)); + + JPanel top = new JPanel(); + top.setBorder(new EmptyBorder(10, 10, 10, 10)); + top.setBackground(SystemColor.window); + panel.add(top, BorderLayout.NORTH); + FlowLayout fl_top = new FlowLayout(FlowLayout.LEADING, 5, 5); + top.setLayout(fl_top); + + JLabel lblCreateNewProject = new JLabel("Create a new Project"); + lblCreateNewProject.setFont(new Font("Tahoma", Font.BOLD, 13)); + top.add(lblCreateNewProject); + + JPanel bottom = new JPanel(); + bottom.setBorder(new EmptyBorder(10, 10, 10, 10)); + FlowLayout fl_bottom = (FlowLayout) bottom.getLayout(); + fl_bottom.setAlignment(FlowLayout.TRAILING); + panel.add(bottom, BorderLayout.SOUTH); + + btnCreate = new JButton("Create"); + btnCreate.setEnabled(false); + bottom.add(btnCreate); + btnCreate.addActionListener(this); + + btnCancel = new JButton("Cancel"); + bottom.add(btnCancel); + btnCancel.addActionListener(this); + + JPanel main = new JPanel(); + main.setBorder(new CompoundBorder( + new MatteBorder(1, 0, 1, 0, new Color(180, 180, 180)), + new EmptyBorder(16, 16, 16, 16) + )); + panel.add(main, BorderLayout.CENTER); + main.setLayout(new BorderLayout(0, 0)); + + JPanel directory = new JPanel(); + directory.setBorder(new EmptyBorder(0, 0, 16, 0)); + main.add(directory, BorderLayout.NORTH); + GridBagLayout gbl_directory = new GridBagLayout(); + gbl_directory.columnWidths = new int[]{0, 0, 0, 0}; + gbl_directory.rowHeights = new int[]{23, 0}; + gbl_directory.columnWeights = new double[]{0.0, 1.0, 0.0, + Double.MIN_VALUE}; + gbl_directory.rowWeights = new double[]{0.0, Double.MIN_VALUE}; + directory.setLayout(gbl_directory); + + JLabel lblDDirectory = new JLabel("Directory:"); + GridBagConstraints gbc_lblDDirectory = new GridBagConstraints(); + gbc_lblDDirectory.anchor = GridBagConstraints.WEST; + gbc_lblDDirectory.insets = new Insets(0, 0, 0, 5); + gbc_lblDDirectory.gridx = 0; + gbc_lblDDirectory.gridy = 0; + directory.add(lblDDirectory, gbc_lblDDirectory); + + tfPath = new JTextField(); + GridBagConstraints gbc_tfPath = new GridBagConstraints(); + gbc_tfPath.fill = GridBagConstraints.HORIZONTAL; + gbc_tfPath.insets = new Insets(0, 0, 0, 5); + gbc_tfPath.gridx = 1; + gbc_tfPath.gridy = 0; + directory.add(tfPath, gbc_tfPath); + tfPath.setColumns(10); + tfPath.getDocument().addDocumentListener(this); + + btnPathSelect = new JButton("Browse..."); + GridBagConstraints gbc_tfPathSelect = new GridBagConstraints(); + gbc_tfPathSelect.anchor = GridBagConstraints.NORTHWEST; + gbc_tfPathSelect.gridx = 2; + gbc_tfPathSelect.gridy = 0; + directory.add(btnPathSelect, gbc_tfPathSelect); + btnPathSelect.addActionListener(this); + + JPanel options = new JPanel(); + options.setBorder(new CompoundBorder(new TitledBorder( + UIManager.getBorder("TitledBorder.border"), "Options", + TitledBorder.LEADING, TitledBorder.TOP, null, + new Color(0, 0, 0)), new EmptyBorder(10, 10, 10, 10))); + main.add(options); + GridBagLayout gbl_options = new GridBagLayout(); + gbl_options.columnWidths = new int[]{50, 47, 0}; + gbl_options.rowHeights = new int[]{23, 0, 0, 0}; + gbl_options.columnWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + gbl_options.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE}; + options.setLayout(gbl_options); + + JLabel lblFileFilter = new JLabel("File types:"); + GridBagConstraints gbc_lblFileFilter = new GridBagConstraints(); + gbc_lblFileFilter.anchor = GridBagConstraints.WEST; + gbc_lblFileFilter.insets = new Insets(0, 0, 5, 5); + gbc_lblFileFilter.gridx = 0; + gbc_lblFileFilter.gridy = 0; + options.add(lblFileFilter, gbc_lblFileFilter); + + cbTiff = new JCheckBox("TIFF"); + cbTiff.setSelected(true); + GridBagConstraints gbc_cbTiff = new GridBagConstraints(); + gbc_cbTiff.anchor = GridBagConstraints.NORTHWEST; + gbc_cbTiff.insets = new Insets(0, 0, 5, 0); + gbc_cbTiff.gridx = 1; + gbc_cbTiff.gridy = 0; + options.add(cbTiff, gbc_cbTiff); + cbTiff.addActionListener(this); + + cbPng = new JCheckBox("PNG"); + cbPng.setSelected(true); + GridBagConstraints gbc_cbPng = new GridBagConstraints(); + gbc_cbPng.anchor = GridBagConstraints.NORTHWEST; + gbc_cbPng.insets = new Insets(0, 0, 5, 0); + gbc_cbPng.gridx = 1; + gbc_cbPng.gridy = 1; + options.add(cbPng, gbc_cbPng); + + cbJpeg = new JCheckBox("JPEG"); + GridBagConstraints gbc_cbJpeg = new GridBagConstraints(); + gbc_cbJpeg.anchor = GridBagConstraints.NORTHWEST; + gbc_cbJpeg.gridx = 1; + gbc_cbJpeg.gridy = 2; + options.add(cbJpeg, gbc_cbJpeg); + cbJpeg.addActionListener(this); + cbPng.addActionListener(this); + + pack(); + setMinimumSize(getSize()); + setSize(new Dimension(450, 0)); + + setLocationRelativeTo(owner); + } + + @Override + public void actionPerformed(ActionEvent evt) { + if (evt.getSource() == btnCancel) { + this.dispose(); + } else if (evt.getSource() == btnCreate) { + // set result if settings are valid + if (isStateValid()) { + Path dir = Paths.get(tfPath.getText()); + this.result = Optional.of(new ProjectModel(dir, + cbTiff.isSelected(), cbPng.isSelected(), + cbJpeg.isSelected())); + } + + this.dispose(); + } else { + if (evt.getSource() == btnPathSelect) { + // show directory chooser + final JFileChooser jfc = new JFileChooser(); + jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + try { + final String defaultDir = getPreferences().get(PREF_DIR, + System.getProperty("user.home")); + + jfc.setCurrentDirectory(new File(defaultDir)); + } catch (Exception e) { + // ignore exception + } + + int result = jfc.showOpenDialog(this); + + if (result == JFileChooser.APPROVE_OPTION) { + tfPath.setText(jfc.getSelectedFile().getAbsolutePath()); + } + } + + // validate state + btnCreate.setEnabled(isStateValid()); + } + } + + private boolean isStateValid() { + // validate the dialog + if (tfPath.getText().isEmpty()) { + return false; + } + + final Path directory = Paths.get(tfPath.getText()); + + return Files.isDirectory(directory) && Files.isReadable(directory) + && (cbTiff.isSelected() || cbPng.isSelected() + || cbJpeg.isSelected()); + } + + private Preferences getPreferences() { + return Preferences.userNodeForPackage(getClass()); + } + + public static Optional showDialog(Window parent) { + final NewProjectDialog dialog = new NewProjectDialog(parent); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + dialog.setVisible(true); + + if (dialog.result.isPresent()) { + dialog.getPreferences().put(PREF_DIR, + dialog.result.get().getImageDir().toString()); + } + + return dialog.result; + } + + @Override + public void changedUpdate(DocumentEvent e) { + btnCreate.setEnabled(isStateValid()); + } + + @Override + public void insertUpdate(DocumentEvent e) { + btnCreate.setEnabled(isStateValid()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + btnCreate.setEnabled(isStateValid()); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java new file mode 100644 index 00000000..f54c69ab --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/PreferencesDialog.java @@ -0,0 +1,227 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import de.vorb.tesseract.gui.model.PreferencesUtil; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Toolkit; +import java.io.File; +import java.util.Arrays; +import java.util.prefs.Preferences; + +public class PreferencesDialog extends JDialog { + private static final long serialVersionUID = 1L; + + private static final String[] PSM_MODES = { + "0 - PSM_OSD_ONLY", + "1 - PSM_AUTO_OSD", + "2 - PSM_AUTO_ONLY", + "3 - (DEFAULT) PSM_AUTO", + "4 - PSM_SINGLE_COLUMN", + "5 - PSM_SINGLE_BLOCK_VERT_TEXT", + "6 - PSM_SINGLE_BLOCK", + "7 - PSM_SINGLE_LINE", + "8 - PSM_SINGLE_WORD", + "9 - PSM_CIRCLE_WORD", + "10 - PSM_SINGLE_CHAR", + "11 - PSM_SPARSE_TEXT", + "12 - PSM_SPARSE_TEXT_OSD", + "13 - PSM_RAW_LINE", + }; + public static final int DEFAULT_PSM_MODE = 3; + + public static final String KEY_LANGDATA_DIR = "langdata_dir"; + public static final String KEY_RENDERING_FONT = "rendering_font"; + public static final String KEY_EDITOR_FONT = "editor_font"; + public static final String KEY_PAGE_SEG_MODE = "page_seg_mode"; + + private final JPanel contentPanel = new JPanel(); + private JTextField tfLangdataDir; + + private final JComboBox comboRenderingFont; + private final JComboBox comboEditorFont; + private final JComboBox comboPageSegMode; + + private ResultState resultState = ResultState.CANCEL; + + public enum ResultState { + APPROVE, CANCEL + } + + /** + * Create the dialog. + */ + public PreferencesDialog() { + setIconImage(Toolkit.getDefaultToolkit().getImage(PreferencesDialog.class.getResource("/logos/logo_16.png"))); + final Preferences pref = PreferencesUtil.getPreferences(); + + setModalityType(ModalityType.APPLICATION_MODAL); + setTitle("General Preferences"); + setBounds(100, 100, 450, 300); + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + GridBagLayout gbl_contentPanel = new GridBagLayout(); + gbl_contentPanel.columnWidths = new int[]{0, 0, 0, 0}; + gbl_contentPanel.rowHeights = new int[]{0, 0, 0}; + gbl_contentPanel.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE}; + gbl_contentPanel.rowWeights = new double[]{0.0, 0.0, Double.MIN_VALUE}; + contentPanel.setLayout(gbl_contentPanel); + { + JLabel lbllangdataDirectory = new JLabel("\"Langdata\" directory:"); + GridBagConstraints gbc_lbllangdataDirectory = new GridBagConstraints(); + gbc_lbllangdataDirectory.anchor = GridBagConstraints.EAST; + gbc_lbllangdataDirectory.insets = new Insets(0, 0, 0, 5); + gbc_lbllangdataDirectory.gridx = 0; + gbc_lbllangdataDirectory.gridy = 1; + contentPanel.add(lbllangdataDirectory, gbc_lbllangdataDirectory); + } + { + tfLangdataDir = new JTextField(pref.get(KEY_LANGDATA_DIR, "")); + GridBagConstraints gbc_textField_1 = new GridBagConstraints(); + gbc_textField_1.insets = new Insets(0, 0, 0, 5); + gbc_textField_1.fill = GridBagConstraints.HORIZONTAL; + gbc_textField_1.gridx = 1; + gbc_textField_1.gridy = 1; + contentPanel.add(tfLangdataDir, gbc_textField_1); + tfLangdataDir.setColumns(30); + } + { + JButton btnSelect_1 = new JButton("Select..."); + btnSelect_1.addActionListener(evt -> { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + try { + final File currentDir = new File( + tfLangdataDir.getText()); + if (currentDir.isDirectory()) { + fc.setCurrentDirectory(currentDir); + } + } catch (Exception e) { + } + + final int result = fc.showOpenDialog(PreferencesDialog.this); + if (result == JFileChooser.APPROVE_OPTION) { + tfLangdataDir.setText(fc.getSelectedFile().getAbsolutePath()); + } + }); + GridBagConstraints gbc_btnSelect_1 = new GridBagConstraints(); + gbc_btnSelect_1.anchor = GridBagConstraints.WEST; + gbc_btnSelect_1.gridx = 2; + gbc_btnSelect_1.gridy = 1; + contentPanel.add(btnSelect_1, gbc_btnSelect_1); + } + { + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); + getContentPane().add(buttonPane, BorderLayout.SOUTH); + { + JButton okButton = new JButton("Save"); + okButton.addActionListener(evt -> { + PreferencesDialog.this.setState(ResultState.APPROVE); + PreferencesDialog.this.dispose(); + }); + okButton.setActionCommand("OK"); + buttonPane.add(okButton); + getRootPane().setDefaultButton(okButton); + } + { + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(evt -> { + PreferencesDialog.this.setState(ResultState.CANCEL); + PreferencesDialog.this.dispose(); + }); + cancelButton.setActionCommand("Cancel"); + buttonPane.add(cancelButton); + } + } + + final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + final String[] availableFontFamilyNames = graphicsEnvironment.getAvailableFontFamilyNames(); + + // Rendering font + addGridLabel(0, 2, "Rendering font:"); + final String initialFontFamilyName = pref.get(PreferencesDialog.KEY_EDITOR_FONT, Font.SANS_SERIF); + comboRenderingFont = createGridComboBox(1, 2, availableFontFamilyNames, initialFontFamilyName); + + // Editor font + addGridLabel(0, 3, "Editor font:"); + final String initialEditorFontFamilyName = pref.get(PreferencesDialog.KEY_RENDERING_FONT, Font.MONOSPACED); + comboEditorFont = createGridComboBox(1, 3, availableFontFamilyNames, initialEditorFontFamilyName); + + // Page Segmentation Modes + addGridLabel(0, 4, "Page Segmentation Mode:"); + final int pageSegMode = pref.getInt(PreferencesDialog.KEY_PAGE_SEG_MODE, DEFAULT_PSM_MODE); + comboPageSegMode = createGridComboBox(1, 4, PSM_MODES, PSM_MODES[pageSegMode]); + + pack(); + setMinimumSize(getSize()); + } + + private void setState(ResultState state) { + this.resultState = state; + } + + public JTextField getTfLangdataDir() { + return tfLangdataDir; + } + + public JComboBox getComboRenderingFont() { + return comboRenderingFont; + } + + public JComboBox getComboEditorFont() { + return comboEditorFont; + } + + public int getPageSegmentationMode() { + return comboPageSegMode.getSelectedIndex(); + } + + public ResultState showPreferencesDialog(Component parent) { + setLocationRelativeTo(parent); + setVisible(true); + return resultState; + } + + private Component addGridLabel(int gridX, int gridY, String label) { + JLabel jLabel = new JLabel(label); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.anchor = GridBagConstraints.EAST; + constraints.insets = new Insets(0, 0, 0, 5); + constraints.gridx = gridX; + constraints.gridy = gridY; + contentPanel.add(jLabel, constraints); + return jLabel; + } + + private JComboBox createGridComboBox(int gridX, int gridY, T[] options, T selectedItem) { + JComboBox jComboBox = new JComboBox<>(); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.insets = new Insets(0, 0, 0, 5); + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.gridx = gridX; + constraints.gridy = gridY; + contentPanel.add(jComboBox, constraints); + + Arrays.stream(options).forEach(jComboBox::addItem); + jComboBox.setSelectedItem(selectedItem); + return jComboBox; + } + +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java new file mode 100644 index 00000000..909e25af --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/dialogs/UnicharsetDebugger.java @@ -0,0 +1,180 @@ +package de.vorb.tesseract.gui.view.dialogs; + +import de.vorb.tesseract.gui.view.TesseractFrame; +import de.vorb.tesseract.gui.view.renderer.UnicharListRenderer; +import de.vorb.tesseract.tools.training.Char; +import de.vorb.tesseract.tools.training.CharacterDimensions; +import de.vorb.tesseract.tools.training.Unicharset; + +import javax.swing.DefaultListModel; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.util.LinkedList; +import java.util.List; + +public class UnicharsetDebugger extends JFrame { + private static final long serialVersionUID = 1L; + + private static final int WIDTH = 800; + private static final int HEIGHT = 256; + private static final int RIGHT = WIDTH - 1; + private static final int TOP = HEIGHT - 1; + + private final JPanel contentPanel = new JPanel(); + + /** + * Create the dialog. + */ + public UnicharsetDebugger(final Unicharset unicharset) { + final Toolkit t = Toolkit.getDefaultToolkit(); + final List appIcons = new LinkedList<>(); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_16.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_96.png"))); + appIcons.add(t.getImage( + TesseractFrame.class.getResource("/logos/logo_256.png"))); + setIconImages(appIcons); + + setTitle("Unicharset Debugger"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setBounds(100, 100, 450, 300); + + getContentPane().setLayout(new BorderLayout()); + contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(new BorderLayout(0, 0)); + + JScrollPane scrollPane = new JScrollPane(); + contentPanel.add(scrollPane, BorderLayout.WEST); + + final JList list = new JList<>(); + list.setCellRenderer(new UnicharListRenderer()); + scrollPane.setViewportView(list); + + final BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT, + BufferedImage.TYPE_INT_RGB); + final Graphics2D g2d = canvas.createGraphics(); + + final Color colorBottom = new Color(0x66FF0000, true); + final Color colorTop = new Color(0x6600FF00, true); + final Color colorWidth = new Color(0x660000FF, true); + + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, WIDTH, HEIGHT); + + final ImageIcon icon = new ImageIcon(canvas); + + JPanel panel = new JPanel(); + contentPanel.add(panel, BorderLayout.CENTER); + panel.setLayout(new BorderLayout(0, 0)); + + final JLabel label = new JLabel(""); + panel.add(label, BorderLayout.CENTER); + label.setHorizontalAlignment(SwingConstants.CENTER); + label.setIcon(icon); + + DefaultListModel listModel = new DefaultListModel<>(); + unicharset.getCharacters().forEach(listModel::addElement); + list.setModel(listModel); + + list.addListSelectionListener(evt -> { + if (evt.getValueIsAdjusting()) { + return; + } + + g2d.clearRect(0, 0, WIDTH, HEIGHT); + + final int index = list.getSelectedIndex(); + final Char unichar = unicharset.getCharacters().get(index); + final CharacterDimensions dims = unichar.getDimensions(); + + final int minBottom = dims.getMinBottom(); + final int maxBottom = dims.getMaxBottom(); + + final int minTop = dims.getMinTop(); + final int maxTop = dims.getMaxTop(); + + final int minWidth = dims.getMinWidth(); + final int maxWidth = dims.getMaxWidth(); + + g2d.setPaint(colorBottom); + g2d.fillRect(0, TOP - maxBottom, WIDTH, maxBottom - minBottom); + + g2d.setPaint(colorTop); + g2d.fillRect(0, TOP - maxTop, WIDTH, maxTop - minTop); + + g2d.setPaint(colorWidth); + g2d.fillRect(minWidth, 0, maxWidth, HEIGHT); + + // TODO show bearing etc + + icon.setImage(canvas); + label.repaint(); + }); + + scrollPane.setViewportView(list); + + // legend + + JPanel panel_1 = new JPanel(); + panel.add(panel_1, BorderLayout.SOUTH); + + final BufferedImage colorBottomImg = new BufferedImage(16, 16, + BufferedImage.TYPE_INT_RGB); + Graphics2D colorG2d = colorBottomImg.createGraphics(); + colorG2d.setBackground(colorBottom); + colorG2d.clearRect(0, 0, 16, 16); + colorG2d.dispose(); + + JLabel lblBottomRange = new JLabel("Bottom Range"); + lblBottomRange.setIcon(new ImageIcon(colorBottomImg)); + panel_1.add(lblBottomRange); + + final BufferedImage colorTopImage = new BufferedImage(16, 16, + BufferedImage.TYPE_INT_RGB); + colorG2d = colorTopImage.createGraphics(); + colorG2d.setBackground(colorTop); + colorG2d.clearRect(0, 0, 16, 16); + colorG2d.dispose(); + + JLabel lblTopRange = new JLabel("Top Range"); + lblTopRange.setIcon(new ImageIcon(colorTopImage)); + panel_1.add(lblTopRange); + + final BufferedImage colorWidthImage = new BufferedImage(16, 16, + BufferedImage.TYPE_INT_RGB); + colorG2d = colorWidthImage.createGraphics(); + colorG2d.setBackground(colorWidth); + colorG2d.clearRect(0, 0, 16, 16); + colorG2d.dispose(); + + JLabel lblWidthRange = new JLabel("Width Range"); + lblWidthRange.setIcon(new ImageIcon(colorWidthImage)); + panel_1.add(lblWidthRange); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent evt) { + g2d.dispose(); + } + }); + + pack(); + setResizable(false); + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java similarity index 89% rename from src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java index f0ebcd03..69fe904a 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/Labels.java @@ -4,7 +4,10 @@ import java.util.MissingResourceException; import java.util.ResourceBundle; -public class Labels { +public final class Labels { + + private Labels() {} + public static String getLabel(Locale locale, String key) { final ResourceBundle labels = ResourceBundle.getBundle("l10n/labels", locale); diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java new file mode 100644 index 00000000..6d2298a5 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Paul Vorbach + */ +/** + * @author Paul Vorbach + * + */ +package de.vorb.tesseract.gui.view.i18n; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java b/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java new file mode 100644 index 00000000..d44e9281 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/package-info.java @@ -0,0 +1,4 @@ +/** + * @author Paul Vorbach + */ +package de.vorb.tesseract.gui.view; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java new file mode 100644 index 00000000..a701c2c9 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java @@ -0,0 +1,155 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.gui.model.BoxFileModel; +import de.vorb.tesseract.gui.view.BoxEditor; +import de.vorb.tesseract.gui.view.Colors; +import de.vorb.tesseract.gui.view.Strokes; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.ListModel; +import javax.swing.SwingWorker; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; + +import static de.vorb.tesseract.gui.model.Scale.scaled; + +public class BoxFileRenderer { + private final BoxEditor boxEditor; + + private SwingWorker renderWorker; + + public BoxFileRenderer(BoxEditor boxEditor) { + this.boxEditor = boxEditor; + } + + public void render(final Optional model, final float scale) { + if (!model.isPresent()) { + // remove image, if no page model is given and free resources + final Icon icon = boxEditor.getCanvas().getIcon(); + if (icon != null && icon instanceof ImageIcon) { + ((ImageIcon) icon).getImage().flush(); + } + + boxEditor.getCanvas().setIcon(null); + + renderWorker = null; + + return; + } + + // TODO add a version of render() that takes two rectangles and a new + // box and updates the necessary area only + final BufferedImage image = model.get().getImage(); + + // calculate image dimensions + final int w = image.getWidth(); + final int h = image.getHeight(); + + final int scaledW = scaled(w, scale); + final int scaledH = scaled(h, scale); + + // try to cancel the last rendering task + if (renderWorker != null && !renderWorker.isCancelled() + && !renderWorker.isDone()) { + renderWorker.cancel(true); + } + + // create an array of all currently visible symbols + final ListModel listModel = + boxEditor.getSymbols().getListModel(); + final Symbol[] symbols = new Symbol[listModel.getSize()]; + + // remember index of selected symbol in the table + final int selectedIndex = + boxEditor.getSymbols().getTable().getSelectedRow(); + + // fill array and save ref to selected symbol + Symbol selSym = null; + for (int i = 0; i < symbols.length; i++) { + final Symbol symbol = listModel.getElementAt(i); + if (selectedIndex == i) { + selSym = symbol; + } + symbols[i] = symbol; + } + final Symbol selectedSymbol = selSym; + + // worker that renders the boxes to a new buffered image + renderWorker = new SwingWorker() { + @Override + protected BufferedImage doInBackground() throws Exception { + final BufferedImage rendered = new BufferedImage(scaledW, + scaledH, BufferedImage.TYPE_INT_RGB); + + final Graphics2D g2d = rendered.createGraphics(); + + // initial color & stroke + g2d.setColor(Colors.NORMAL); + g2d.setStroke(Strokes.NORMAL); + + // draw the scaled image + g2d.drawImage(image, 0, 0, scaledW, scaledH, 0, 0, + w - 1, h - 1, null); + + for (final Symbol symbol : symbols) { + final boolean isSelected = symbol == selectedSymbol; + // draw box on canvas + drawSymbolBox(g2d, symbol, scale, isSelected); + } + + // dispose context + g2d.dispose(); + + return rendered; + } + + @Override + public void done() { + try { + // draw the rendered image + boxEditor.getCanvas().setIcon(new ImageIcon(get())); + } catch (InterruptedException | ExecutionException + | CancellationException e) { + // ignore interrupts of any kind, those are intended + } finally { + System.gc(); + } + } + }; + + renderWorker.execute(); + } + + private void drawSymbolBox(final Graphics2D g2d, final Symbol symbol, + final float scale, final boolean isSelected) { + final Box boundingBox = symbol.getBoundingBox(); + + // set selected colors + if (isSelected) { + g2d.setPaint(Colors.SELECTION_BG); + g2d.fillRect(scaled(boundingBox.getX(), scale), + scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), + scaled(boundingBox.getHeight(), scale)); + + g2d.setPaint(Colors.SELECTION); + g2d.setStroke(Strokes.SELECTION); + } + + // draw the box + g2d.drawRect(scaled(boundingBox.getX(), scale), scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), scaled(boundingBox.getHeight(), scale)); + + // unset selected colors + if (isSelected) { + g2d.setColor(Colors.NORMAL); + g2d.setStroke(Strokes.NORMAL); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java new file mode 100644 index 00000000..c2551d8c --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/EvaluationPaneRenderer.java @@ -0,0 +1,44 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.model.Scale; +import de.vorb.tesseract.gui.view.EvaluationPane; + +import javax.swing.ImageIcon; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.Optional; + +public class EvaluationPaneRenderer implements PageRenderer { + private final EvaluationPane evaluationPane; + + public EvaluationPaneRenderer(EvaluationPane evaluationPane) { + this.evaluationPane = evaluationPane; + } + + @Override + public void render(Optional pageModel, float scale) { + final boolean modelPresent = pageModel.isPresent(); + + evaluationPane.getSaveTranscriptionButton().setEnabled(modelPresent); + evaluationPane.getGenerateReportButton().setEnabled(modelPresent); + evaluationPane.getTextAreaTranscript().setEnabled(modelPresent); + + if (modelPresent) { + final BufferedImage img = + pageModel.get().getImageModel().getSourceImage(); + + final int w = Scale.scaled(img.getWidth(), scale); + final int h = Scale.scaled(img.getHeight(), scale); + + evaluationPane.getOriginal().setIcon( + new ImageIcon(img.getScaledInstance(w, h, + Image.SCALE_SMOOTH))); + evaluationPane.getTextAreaTranscript().setText( + pageModel.get().getTranscription()); + } else { + evaluationPane.getOriginal().setIcon(null); + evaluationPane.getTextAreaTranscript().setText(""); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java new file mode 100644 index 00000000..43169957 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageListCellRenderer.java @@ -0,0 +1,90 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.gui.model.PageThumbnail; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.Optional; + +public class PageListCellRenderer extends JLabel implements + ListCellRenderer { + private static final long serialVersionUID = 1L; + + private static final ImageIcon ICON_PLACEHOLDER = new ImageIcon( + PageListCellRenderer.class.getResource("/page_loading.png")); + + public static final Color COLOR_SELECT = new Color(0x4477FF); + + public PageListCellRenderer() { + setOpaque(true); + + setVerticalTextPosition(BOTTOM); + setHorizontalTextPosition(CENTER); + + setIconTextGap(5); + + // 10px empty border + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + } + + @Override + public Component getListCellRendererComponent( + JList list, PageThumbnail value, + int index, boolean isSelected, boolean cellHasFocus) { + String filename = value.getFile().getFileName().toString(); + if (filename.length() > 32) { + filename = filename.substring(0, 10) + "..." + + filename.substring(filename.length() - 16); + } + + setText(filename); + + if (isSelected) { + setBackground(COLOR_SELECT); + setForeground(Color.WHITE); + } else { + setBackground(Color.WHITE); + setForeground(Color.BLACK); + } + + final Optional opt = value.getThumbnail(); + + if (opt.isPresent()) { + final BufferedImage thumbnail = opt.get(); + final Graphics2D g2d = (Graphics2D) thumbnail.getGraphics(); + + // set color + if (thumbnail.getType() != BufferedImage.TYPE_BYTE_BINARY) { + if (isSelected) { + g2d.setColor(Color.BLACK); + } else { + g2d.setColor(Color.GRAY); + } + } else { + g2d.setColor(Color.BLACK); + } + + g2d.drawRect(0, 0, thumbnail.getWidth() - 1, + thumbnail.getHeight() - 1); + g2d.dispose(); + + setIcon(new ImageIcon(thumbnail)); + } else { + setIcon(ICON_PLACEHOLDER); + } + + return this; + } + + @Override + public int getHorizontalAlignment() { + return CENTER; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java new file mode 100644 index 00000000..cd2267bf --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java @@ -0,0 +1,21 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.gui.model.PageModel; + +import java.util.Optional; + +/** + * Page renderer. + * + * @author Paul Vorbach + */ +public interface PageRenderer { + + /** + * Renders the information of a page on an optionally given background. + * + * @param pageModel page model to render + * @param scale scaling factor + */ + void render(final Optional pageModel, final float scale); +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java new file mode 100644 index 00000000..cd01fb06 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/RecognitionRenderer.java @@ -0,0 +1,354 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.view.Colors; +import de.vorb.tesseract.gui.view.RecognitionPane; +import de.vorb.tesseract.util.Baseline; +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.FontAttributes; +import de.vorb.tesseract.util.Line; +import de.vorb.tesseract.util.Page; +import de.vorb.tesseract.util.Paragraph; +import de.vorb.tesseract.util.Symbol; +import de.vorb.tesseract.util.Word; + +import javax.swing.ImageIcon; +import javax.swing.SwingWorker; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.Iterator; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import static de.vorb.tesseract.gui.model.Scale.scaled; + +public class RecognitionRenderer implements PageRenderer { + + private static final int DEFAULT_FONT_SIZE = 12; + + private static final Font FONT_LINE_NUMBERS = new Font("Dialog", Font.PLAIN, DEFAULT_FONT_SIZE); + + private final RecognitionPane recognitionPane; + private SwingWorker renderWorker; + + private PageModel lastPageModel = null; + private BufferedImage original = null; + private BufferedImage recognition = null; + private float minimumConfidence = 0; + private float lastScale; + + private final AtomicReference fontNormal = new AtomicReference<>(); + private final AtomicReference fontBold = new AtomicReference<>(); + private final AtomicReference fontItalic = new AtomicReference<>(); + private final AtomicReference fontBoldItalic = new AtomicReference<>(); + + public RecognitionRenderer(RecognitionPane pane, String renderingFont) { + recognitionPane = pane; + + setRenderingFont(renderingFont); + } + + public void setRenderingFont(String renderingFont) { + final String selectedFontFamily = renderingFont; + + fontNormal.set(new Font(selectedFontFamily, Font.PLAIN, DEFAULT_FONT_SIZE)); + fontBold.set(new Font(selectedFontFamily, Font.BOLD, DEFAULT_FONT_SIZE)); + fontItalic.set(new Font(selectedFontFamily, Font.ITALIC, DEFAULT_FONT_SIZE)); + fontBoldItalic.set(new Font(selectedFontFamily, Font.BOLD | Font.ITALIC, DEFAULT_FONT_SIZE)); + } + + public void setMinimumConfidence(float min) { + this.minimumConfidence = min; + } + + @Override + public void render(Optional pageModel, final float scale) { + if (renderWorker != null && !renderWorker.isCancelled() + && !renderWorker.isDone()) { + renderWorker.cancel(true); + } + + // if no page model is present, remove the images and render worker + if (!pageModel.isPresent()) { + renderWorker = null; + + recognitionPane.getCanvasOriginal().setIcon(null); + recognitionPane.getCanvasRecognition().setIcon(null); + + original = null; + recognition = null; + + return; + } + + final Page page = pageModel.get().getPage(); + final BufferedImage preprocessed = + pageModel.get().getImageModel().getPreprocessedImage(); + final int width = preprocessed.getWidth(); + final int height = preprocessed.getHeight(); + + final int scaledWidth; + final int scaledHeight; + if (lastPageModel != pageModel.get() || lastScale != scale) { + // prepare the images if the model has changed + + lastPageModel = pageModel.get(); + lastScale = scale; + + // calculate the width and height of the scene + scaledWidth = scaled(width, scale); + scaledHeight = scaled(height, scale); + + // create empty images + original = new BufferedImage(scaledWidth, scaledHeight, + BufferedImage.TYPE_INT_RGB); + recognition = new BufferedImage(scaledWidth, scaledHeight, + BufferedImage.TYPE_INT_RGB); + } else { + // otherwise + scaledWidth = original.getWidth(); + scaledHeight = original.getHeight(); + } + + // set the base fonts + final Font baseFontNormal; + final Font baseFontItalic; + final Font baseFontBold; + final Font baseFontBoldItalic; + + final boolean showWordBoxes = recognitionPane.getWordBoxes().isSelected(); + final boolean showSymbolBoxes = recognitionPane.getSymbolBoxes().isSelected(); + final boolean showLineNumbers = recognitionPane.getLineNumbers().isSelected(); + final boolean showBaselines = recognitionPane.getBaselines().isSelected(); + // final boolean showXLines = recognitionPane.getXLines().isSelected(); + final boolean showBlocks = recognitionPane.getBlocks().isSelected(); + final boolean showParagraphs = recognitionPane.getParagraphs().isSelected(); + + renderWorker = new SwingWorker() { + private Graphics2D origGfx, recogGfx; + + private void drawLineNumber(final Line line, final int lineNumber) { + + final Box box = line.getBoundingBox(); + + final String num = String.valueOf(lineNumber); + final int x = scaled(20, scale); + final int y = scaled(box.getY() + box.getHeight() + - line.getBaseline().getYOffset(), scale); + + origGfx.setFont(FONT_LINE_NUMBERS); + origGfx.setPaint(Colors.LINE_NUMBER); + origGfx.drawString(num, x, y); + + recogGfx.setFont(FONT_LINE_NUMBERS); + recogGfx.setPaint(Colors.LINE_NUMBER); + recogGfx.drawString(num, x, y); + } + + private void drawWord(final Line line, final Word word) { + // bounding box + final Box box = word.getBoundingBox(); + + // baseline + final Baseline bl = word.getBaseline(); + + // font attributes + final FontAttributes fa = word.getFontAttributes(); + + // scaled font size + final float scFontSize = scaled(fa.getSize(), scale); + + // bold? + final boolean bold = fa.isBold(); + // italic? + final boolean italic = fa.isItalic(); + + // selected? + // final boolean isSelected = word.isSelected(); + + // coordinates + final int bX = box.getX(), bY = box.getY(); + final int bW = box.getWidth(), bH = box.getHeight(); + + // scaled coordinates + final int scX = scaled(bX, scale); + final int scY = scaled(bY, scale); + final int scW = scaled(bW, scale); + final int scH = scaled(bH, scale); + + // text coordinates + final int tx = scX; + final int ty = scaled(bY + bH - word.getBaseline().getYOffset(), scale); + + // set font + final Font font; + if (!italic && !bold) { + font = fontNormal.get().deriveFont(scFontSize); + } else if (italic && !bold) { + font = fontItalic.get().deriveFont(scFontSize); + } else if (bold && !italic) { + font = fontBold.get().deriveFont(scFontSize); + } else { + font = fontBoldItalic.get().deriveFont(scFontSize); + } + + recogGfx.setFont(font); + + if (showWordBoxes || showSymbolBoxes) { + origGfx.setPaint(Colors.NORMAL); + recogGfx.setPaint(Colors.NORMAL); + + if (showWordBoxes) { + origGfx.drawRect(scX, scY, scW, scH); + recogGfx.drawRect(scX, scY, scW, scH); + } else if (showSymbolBoxes) { + for (final Symbol sym : word.getSymbols()) { + + final Box symbolBoundingBox = sym.getBoundingBox(); + final String symbolText = sym.getText(); + + // coordinates + final int sbX = symbolBoundingBox.getX(); + final int sbY = symbolBoundingBox.getY(); + final int sbW = symbolBoundingBox.getWidth(); + final int sbH = symbolBoundingBox.getHeight(); + + // scaled coordinates + final int ssbX = scaled(sbX, scale); + final int ssbY = scaled(sbY, scale); + final int ssbW = scaled(sbW, scale); + final int ssbH = scaled(sbH, scale); + + recogGfx.setPaint(Colors.NORMAL); + + origGfx.drawRect(ssbX, ssbY, ssbW, ssbH); + recogGfx.drawRect(ssbX, ssbY, ssbW, ssbH); + + recogGfx.setPaint(Colors.TEXT); + + recogGfx.drawString(symbolText, ssbX, + scaled(box.getY() + box.getHeight() - word.getBaseline().getYOffset(), scale)); + } + } + } + + if (!showSymbolBoxes) { + recogGfx.setPaint(Colors.TEXT); + + // only draw the string + recogGfx.drawString(word.getText(), tx, ty); + } + + if (showBaselines) { + final int x2 = bX + bW; + final int y1 = bY + bH - bl.getYOffset(); + final int y2 = Math.round(y1 + bW * bl.getSlope()); + + origGfx.setPaint(Colors.BASELINE); + origGfx.drawLine(scaled(bX, scale), scaled(y1, scale), + scaled(x2, scale), scaled(y2, scale)); + recogGfx.setPaint(Colors.BASELINE); + recogGfx.drawLine(scaled(bX, scale), scaled(y1, scale), + scaled(x2, scale), scaled(y2, scale)); + } + } + + @Override + protected Void doInBackground() throws Exception { + // init graphics contexts + origGfx = original.createGraphics(); + recogGfx = recognition.createGraphics(); + + // draw the preprocessed image on original + origGfx.drawImage(preprocessed, 0, 0, scaledWidth, + scaledHeight, 0, 0, width - 1, height - 1, null); + + // clear recognition + recogGfx.setColor(Color.WHITE); + recogGfx.fillRect(0, 0, scaledWidth, scaledHeight); + + // stays the same for all lines + origGfx.setFont(FONT_LINE_NUMBERS); + + if (showBlocks) { + origGfx.setPaint(Colors.BLOCK); + recogGfx.setPaint(Colors.BLOCK); + final Iterator blocks = page.blockIterator(); + while (blocks.hasNext()) { + final Block block = blocks.next(); + final Box boundingBox = block.getBoundingBox(); + origGfx.drawRect(scaled(boundingBox.getX(), scale), + scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), + scaled(boundingBox.getHeight(), scale)); + recogGfx.drawRect(scaled(boundingBox.getX(), scale), + scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), + scaled(boundingBox.getHeight(), scale)); + } + } + + if (showParagraphs) { + origGfx.setPaint(Colors.PARAGRAPH); + recogGfx.setPaint(Colors.PARAGRAPH); + final Iterator paragraphs = page.paragraphIterator(); + while (paragraphs.hasNext()) { + final Paragraph paragraph = paragraphs.next(); + final Box boundingBox = paragraph.getBoundingBox(); + origGfx.drawRect(scaled(boundingBox.getX(), scale), + scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), + scaled(boundingBox.getHeight(), scale)); + recogGfx.drawRect(scaled(boundingBox.getX(), scale), + scaled(boundingBox.getY(), scale), + scaled(boundingBox.getWidth(), scale), + scaled(boundingBox.getHeight(), scale)); + } + } + + int lineNumber = 1; + final Iterator lines = page.lineIterator(); + while (lines.hasNext()) { + final Line line = lines.next(); + if (scale >= 0.5f && showLineNumbers) { + drawLineNumber(line, lineNumber); + } + + recogGfx.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + + for (final Word word : line.getWords()) { + drawWord(line, word); + } + + lineNumber++; + } + + return null; + } + + @Override + protected void done() { + try { + recognitionPane.getCanvasOriginal().setIcon(new ImageIcon(original)); + recognitionPane.getCanvasRecognition().setIcon( + new ImageIcon(recognition)); + } catch (Exception e) { + } finally { + System.gc(); + } + } + }; + + renderWorker.execute(); + } + + public void freeResources() { + lastPageModel = null; + } +} diff --git a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java similarity index 76% rename from src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java rename to gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java index 77f386a1..141663b1 100644 --- a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphSelectionRenderer.java +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolGroupListCellRenderer.java @@ -1,21 +1,20 @@ package de.vorb.tesseract.gui.view.renderer; -import java.awt.Component; -import java.util.Map.Entry; -import java.util.Set; +import de.vorb.tesseract.util.Symbol; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.ListCellRenderer; +import java.awt.Component; +import java.util.List; +import java.util.Map.Entry; -import de.vorb.tesseract.util.Symbol; - -public class GlyphSelectionRenderer extends JLabel implements - ListCellRenderer>> { +public class SymbolGroupListCellRenderer extends JLabel implements + ListCellRenderer>> { private static final long serialVersionUID = 1L; - public GlyphSelectionRenderer() { + public SymbolGroupListCellRenderer() { super(); setOpaque(true); setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); @@ -23,8 +22,8 @@ public GlyphSelectionRenderer() { @Override public Component getListCellRendererComponent( - JList>> list, - Entry> value, int index, boolean isSelected, + JList>> list, + Entry> value, int index, boolean isSelected, boolean cellHasFocus) { if (isSelected) { setBackground(list.getSelectionBackground()); @@ -46,8 +45,7 @@ public Component getListCellRendererComponent( charCodes.append(']'); // Set the icon and text. If icon was null, say so. - setText(symbol + " (" + value.getValue().size() - + ")"); + setText(symbol + " (" + value.getValue().size() + ")"); setToolTipText(charCodes.toString()); return this; diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java new file mode 100644 index 00000000..15f4ca9d --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/SymbolVariantListCellRenderer.java @@ -0,0 +1,48 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Symbol; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import java.awt.Component; +import java.awt.image.BufferedImage; + +public class SymbolVariantListCellRenderer extends JLabel implements + ListCellRenderer { + private static final long serialVersionUID = 1L; + + private final BufferedImage source; + + public SymbolVariantListCellRenderer(BufferedImage source) { + super(); + this.source = source; + setOpaque(true); + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + } + + @Override + public Component getListCellRendererComponent(JList list, + Symbol value, int index, boolean isSelected, boolean cellHasFocus) { + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + final Box boundingBox = value.getBoundingBox(); + final BufferedImage subImage = source.getSubimage(boundingBox.getX(), + boundingBox.getY(), boundingBox.getWidth(), boundingBox.getHeight()); + + setIcon(new ImageIcon(subImage)); + setToolTipText(String.format("confidence = %.2f%%", + value.getConfidence())); + + return this; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java new file mode 100644 index 00000000..8bf9991c --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/view/renderer/UnicharListRenderer.java @@ -0,0 +1,28 @@ +package de.vorb.tesseract.gui.view.renderer; + +import de.vorb.tesseract.tools.training.Char; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; +import java.awt.Component; + +public class UnicharListRenderer extends DefaultListCellRenderer { + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + + // use the char text if value is of type char + if (value instanceof Char) { + return super.getListCellRendererComponent(list, + ((Char) value).getText(), index, isSelected, + cellHasFocus); + } + + return super.getListCellRendererComponent(list, value, index, + isSelected, + cellHasFocus); + } + +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java b/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java new file mode 100644 index 00000000..58ca49e8 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/BatchExecutor.java @@ -0,0 +1,298 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.controller.TesseractController; +import de.vorb.tesseract.gui.model.BatchExportModel; +import de.vorb.tesseract.gui.model.ProjectModel; +import de.vorb.tesseract.gui.util.DocumentWriter; +import de.vorb.tesseract.gui.view.dialogs.Dialogs; +import de.vorb.tesseract.tools.preprocessing.Preprocessor; +import de.vorb.tesseract.util.TraineddataFiles; + +import eu.digitisation.input.Batch; +import eu.digitisation.input.Parameters; +import eu.digitisation.input.WarningException; +import eu.digitisation.output.Report; + +import javax.swing.ProgressMonitor; +import javax.swing.SwingUtilities; +import javax.xml.transform.TransformerException; +import java.awt.Desktop; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +public class BatchExecutor { + private final TesseractController controller; + private final ProjectModel project; + private final BatchExportModel export; + + public BatchExecutor(TesseractController controller, ProjectModel project, + BatchExportModel export) { + + this.controller = controller; + this.project = project; + this.export = export; + } + + public void start(final ProgressMonitor progressMonitor, + final Writer errorLog) throws IOException, InterruptedException { + final int numThreads = export.getNumThreads(); + + final ExecutorService threadPool = + Executors.newFixedThreadPool(numThreads); + + final LinkedBlockingQueue recognizers = + new LinkedBlockingQueue<>(numThreads); + + final String trainingFile = controller.getTrainingFile().get(); + for (int i = 0; i < numThreads; i++) { + final PageRecognitionProducer recognizer = + new PageRecognitionProducer(controller, + TraineddataFiles.getTessdataDir(), + trainingFile); + recognizer.init(); + recognizers.put(recognizer); + } + + final List> futures = new ArrayList<>(); + + // ensure the destination directory and others exist + Files.createDirectories(project.getPreprocessedDir()); + Files.createDirectories(project.getEvaluationDir()); + Files.createDirectories(project.getTranscriptionDir()); + Files.createDirectories(project.getOCRDir()); + Files.createDirectories(export.getDestinationDir()); + + // holds progress count + final AtomicInteger progress = new AtomicInteger(0); + + // holds error count + final AtomicInteger errors = new AtomicInteger(0); + + // prepare reports + final Path equivalencesFile = controller.prepareReports(); + + // create tasks and submit them + for (final Path sourceFile : project.getImageFiles()) { + final Preprocessor preprocessor = + controller.getPreprocessor(sourceFile); + + final boolean hasPreprocessorChanged = + controller.hasPreprocessorChanged(sourceFile); + + final OCRTask task = new OCRTask(sourceFile, + project, export, preprocessor, recognizers, + hasPreprocessorChanged, equivalencesFile, progressMonitor, + progress, errorLog, errors); + + futures.add(threadPool.submit(task)); + } + + final Future all = new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + int cancelled = 0; + + for (Future future : futures) { + if (future.cancel(mayInterruptIfRunning)) + cancelled++; + } + + return cancelled > 0; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + for (Future future : futures) { + future.get(); + } + + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + + // end time + final long end = System.currentTimeMillis() + + unit.toMillis(timeout); + + // while it has not timed out + while (System.currentTimeMillis() < end) { + // when all tasks are completed, return + if (isDone()) { + return null; + } + + // wait 100ms before rechecking + Thread.currentThread().wait(100); + } + + throw new TimeoutException(); + } + + @Override + public boolean isCancelled() { + int cancelled = 0; + + for (Future future : futures) { + if (future.isCancelled()) { + cancelled++; + } + } + + return cancelled == futures.size(); + } + + @Override + public boolean isDone() { + int done = 0; + + for (Future future : futures) { + if (future.isDone()) { + done++; + } + } + + return done == futures.size(); + } + }; + + threadPool.submit(new CompletionCallback(all, progressMonitor, + progress, errorLog, errors)); + + // prevent new tasks, the threads will be garbage collected once all + // tasks have completed + threadPool.shutdown(); + } + + private class CompletionCallback implements Callable { + Future all; + final ProgressMonitor progressMonitor; + final AtomicInteger progress; + final Writer errorLog; + final AtomicInteger errors; + + CompletionCallback(Future all, ProgressMonitor progressMonitor, + AtomicInteger progress, Writer errorLog, AtomicInteger errors) { + this.all = all; + this.progressMonitor = progressMonitor; + this.progress = progress; + this.errorLog = errorLog; + this.errors = errors; + } + + @Override + public Void call() throws IOException { + // wait for all other tasks to complete + try { + all.get(); + } catch (InterruptedException | ExecutionException e) { + } finally { + all = null; + if (export.exportReports()) { + progressMonitor.setNote("Final report"); + // create a single report for all transcriptions + DirectoryStream transcriptions; + try { + transcriptions = Files.newDirectoryStream( + project.getTranscriptionDir()); + + final Batch batch = new Batch(transcriptions, + project.getOCRDir()); + + final Parameters params = new Parameters(); + final Path eqFile = controller.prepareReports(); + params.eqfile.setValue(eqFile.toFile()); + + final Report report = new Report(batch, params); + + final Path projectReport = export.getDestinationDir() + .resolve("project.report.html"); + + // write report html + DocumentWriter.writeToFile(report.document(), projectReport); + + // write report csv + final BufferedWriter csv = Files.newBufferedWriter( + export.getDestinationDir().resolve( + "project.report.csv"), + StandardCharsets.UTF_8); + csv.write(report.getStats().asCSV("\n", ",").toString()); + csv.close(); + } catch (IOException | WarningException + | TransformerException e) { + errors.incrementAndGet(); + + errorLog.write(e.getMessage()); + errorLog.write(System.lineSeparator()); + } + } + + if (export.openDestination()) { + try { + Desktop.getDesktop().browse( + export.getDestinationDir().toUri()); + } catch (IOException e) { + errors.incrementAndGet(); + + errorLog.write(e.getMessage()); + errorLog.write(System.lineSeparator()); + } + } + + progressMonitor.setProgress(progress.incrementAndGet()); + errorLog.close(); + + // show errors or success dialog + SwingUtilities.invokeLater(() -> { + if (errors.get() == 0) { + Dialogs.showInfo(controller.getView(), + "Export completed", + "The batch export finished without any errors."); + } else { + final boolean investigate = Dialogs.ask( + controller.getView(), + "Errors during export", + String.format( + "The batch export finished, but there have been %d errors. Do you want to" + + " investigate the error log?", + errors.get())); + + if (investigate) { + try { + Desktop.getDesktop().open( + export.getDestinationDir().resolve( + "errors.log").toFile()); + } catch (IOException e) { + Dialogs.showError(controller.getView(), + "Error", + "Could not open the error log."); + } + } + } + }); + } + + return null; + } + } + +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/OCRTask.java b/gui/src/main/java/de/vorb/tesseract/gui/work/OCRTask.java new file mode 100644 index 00000000..48ae8616 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/OCRTask.java @@ -0,0 +1,221 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.io.PlainTextWriter; +import de.vorb.tesseract.gui.model.BatchExportModel; +import de.vorb.tesseract.gui.model.ProjectModel; +import de.vorb.tesseract.gui.util.DocumentWriter; +import de.vorb.tesseract.tools.preprocessing.Preprocessor; +import de.vorb.tesseract.tools.recognition.PageRecognitionConsumer; +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Page; +import de.vorb.util.FileNames; + +import eu.digitisation.input.Batch; +import eu.digitisation.input.Parameters; +import eu.digitisation.output.Report; + +import javax.imageio.ImageIO; +import javax.swing.ProgressMonitor; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +public class OCRTask implements Callable { + private final Path sourceFile; + private final ProjectModel project; + private final BatchExportModel export; + private final Preprocessor preprocessor; + private final LinkedBlockingQueue recognizers; + private final boolean hasPreprocessorChanged; + private final Path equivalencesFile; + + private final ProgressMonitor progressMonitor; + private final AtomicInteger progress; + + private final Writer errorLog; + private final AtomicInteger errors; + + public OCRTask(Path sourceFile, ProjectModel project, + BatchExportModel export, Preprocessor preprocessor, + LinkedBlockingQueue recognizers, + boolean hasPreprocessorChanged, Path equivalencesFile, + ProgressMonitor progressMonitor, AtomicInteger progress, + Writer errorLog, AtomicInteger errors) { + this.sourceFile = sourceFile; + this.project = project; + this.export = export; + this.preprocessor = preprocessor; + this.recognizers = recognizers; + this.hasPreprocessorChanged = hasPreprocessorChanged; + this.equivalencesFile = equivalencesFile; + + this.progressMonitor = progressMonitor; + this.progress = progress; + + this.errorLog = errorLog; + this.errors = errors; + } + + @Override + public Void call() throws IOException { + if (progressMonitor.isCanceled()) { + return null; + } + + progressMonitor.setNote(sourceFile.getFileName().toString()); + + final Path imgDestFile = project.getPreprocessedDir().resolve( + FileNames.replaceExtension(sourceFile, "png").getFileName()); + + try { + + final int width; + final int height; + { + final BufferedImage sourceImg = + ImageIO.read(sourceFile.toFile()); + + width = sourceImg.getWidth(); + height = sourceImg.getHeight(); + + final BufferedImage binaryImg; + if (!hasPreprocessorChanged && Files.exists(imgDestFile)) { + // read existing preprocessed image + binaryImg = ImageIO.read(imgDestFile.toFile()); + } else { + // pre-process source image + binaryImg = preprocessor.process(sourceImg); + + ImageIO.write(binaryImg, "PNG", imgDestFile.toFile()); + } + + // optionally copy preprocessed images to the destination + // directory + if (export.exportImages()) { + Files.copy( + imgDestFile, + export.getDestinationDir().resolve( + imgDestFile.getFileName()), + StandardCopyOption.REPLACE_EXISTING); + } + } + + if (Thread.currentThread().isInterrupted()) { + return null; + } + + final PageRecognitionProducer recognizer = recognizers.take(); + if (recognizer == null) { + throw new IllegalStateException( + "No more PageRecognitionProducers"); + } + + try { + recognizer.reset(); + recognizer.loadImage(imgDestFile); + + final List blocks = new ArrayList<>(); + + recognizer.recognize(new PageRecognitionConsumer(blocks) { + @Override + public boolean isCancelled() { + return Thread.currentThread().isInterrupted(); + } + }); + + final Page page = new Page(sourceFile, width, height, 300, + blocks); + + if (export.exportXML()) { + final Path xmlFile = export.getDestinationDir().resolve( + FileNames.replaceExtension(sourceFile, "xml").getFileName()); + + try (BufferedOutputStream out = new BufferedOutputStream(Files.newOutputStream(xmlFile))) { + page.writeTo(out); + } + } + + if (export.exportHTML()) { + // TODO + } + + // write the ocr result as text + final Path txtFile = project.getOCRDir().resolve( + FileNames.replaceExtension(sourceFile, "txt").getFileName()); + { + final Writer out = Files.newBufferedWriter(txtFile, + StandardCharsets.UTF_8); + + new PlainTextWriter(true).write(page, out); + + out.close(); + + // if text files are exported, copy it over + if (export.exportTXT()) { + Files.copy( + txtFile, + export.getDestinationDir().resolve( + txtFile.getFileName()), + StandardCopyOption.REPLACE_EXISTING); + } + } + + if (export.exportReports()) { + final Path transcriptionFile = + project.getTranscriptionDir().resolve( + txtFile.getFileName()); + + if (Files.isRegularFile(transcriptionFile)) { + // generate report + final Batch reportBatch = new Batch( + transcriptionFile.toFile(), txtFile.toFile()); + final Parameters pars = new Parameters(); + pars.eqfile.setValue(equivalencesFile.toFile()); + Report rep; + rep = new Report(reportBatch, pars); + + // write to file + DocumentWriter.writeToFile( + rep.document(), + export.getDestinationDir().resolve( + FileNames.replaceExtension( + sourceFile, "report.html").getFileName())); + + // write csv file + final BufferedWriter csv = Files.newBufferedWriter( + export.getDestinationDir().resolve( + FileNames.replaceExtension( + sourceFile, "report.csv").getFileName()), + StandardCharsets.UTF_8); + + csv.write(rep.getStats().asCSV("\n", ",").toString()); + csv.close(); + } + } + } finally { + recognizers.put(recognizer); + + // update progress + progressMonitor.setProgress(progress.incrementAndGet()); + } + } catch (Throwable e) { + errorLog.write(e.getMessage()); + errorLog.write(System.lineSeparator()); + + errors.incrementAndGet(); + } + + return null; + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/PageListWorker.java b/gui/src/main/java/de/vorb/tesseract/gui/work/PageListWorker.java new file mode 100644 index 00000000..30bf4d7a --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/PageListWorker.java @@ -0,0 +1,42 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.model.PageThumbnail; +import de.vorb.tesseract.gui.model.ProjectModel; + +import javax.swing.DefaultListModel; +import javax.swing.SwingWorker; +import java.awt.image.BufferedImage; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +public class PageListWorker extends SwingWorker { + private final ProjectModel projectModel; + private final DefaultListModel pages; + + public PageListWorker(final ProjectModel projectModel, + DefaultListModel pages) { + this.projectModel = projectModel; + this.pages = pages; + + pages.clear(); + } + + @Override + protected Void doInBackground() throws Exception { + // no thumbnail + final Optional thumbnail = Optional.empty(); + + // publish a placeholder (no thumbnail) for every image file + for (final Path file : projectModel.getImageFiles()) { + publish(new PageThumbnail(file, thumbnail)); + } + + return null; + } + + @Override + protected void process(List chunks) { + chunks.forEach(pages::addElement); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/PageRecognitionProducer.java b/gui/src/main/java/de/vorb/tesseract/gui/work/PageRecognitionProducer.java new file mode 100644 index 00000000..bb3e50ed --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/PageRecognitionProducer.java @@ -0,0 +1,178 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.controller.TesseractController; +import de.vorb.tesseract.tools.recognition.RecognitionProducer; +import de.vorb.tesseract.util.feat.Feature3D; + +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.IntPointer; +import org.bytedeco.javacpp.lept; +import org.bytedeco.javacpp.tesseract; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; + +public class PageRecognitionProducer extends RecognitionProducer { + private final Path tessdataDir; + private Optional lastPix = Optional.empty(); + + private final TesseractController controller; + private final HashMap variables = new HashMap<>(); + private int pageSegmentationMode = tesseract.PSM_AUTO; + + public PageRecognitionProducer(TesseractController controller, + Path tessdataDir, String trainingFile) { + super(trainingFile); + + this.controller = controller; + this.tessdataDir = tessdataDir; + + // save choices for choice iterator + variables.put("save_blob_choices", "T"); + + // heavy noise reduction + // variables.put("textord_heavy_nr", "T"); + + // language_model_penalty_non_dict_word + variables.put("language_model_penalty_non_dict_word", "0.3"); + + // blacklist doesn't work + // variables.put("tessedit_char_blacklist", "=§«°·»¼ÃÆØå¼½æàâèéøɔ$"); + } + + @Override + public void init() throws IOException { + setHandle(tesseract.TessBaseAPICreate()); + + reset(); + } + + @Override + public void reset() throws IOException { + // init LibTess with data path, language and OCR engine mode + tesseract.TessBaseAPIInit2(getHandle(), + tessdataDir.toString(), + getTrainingFile(), + tesseract.OEM_DEFAULT); + + // set page segmentation mode + tesseract.TessBaseAPISetPageSegMode(getHandle(), pageSegmentationMode); + + // set variables + for (Entry var : variables.entrySet()) { + tesseract.TessBaseAPISetVariable(getHandle(), var.getKey(), var.getValue()); + } + } + + @Override + public void close() throws IOException { + tesseract.TessBaseAPIDelete(getHandle()); + } + + public void setPageSegmentationMode(int pageSegmentationMode) { + this.pageSegmentationMode = pageSegmentationMode; + tesseract.TessBaseAPISetPageSegMode(getHandle(), pageSegmentationMode); + } + + public void loadImage(Path imageFile) { + if (lastPix.isPresent()) { + // destroy old pix + lept.pixDestroy(lastPix.get()); + } + + final lept.PIX pix = lept.pixRead(imageFile.toString()); + + tesseract.TessBaseAPISetImage2(getHandle(), pix); + + lastPix = Optional.of(pix); + } + + public Optional getImage() { + return lastPix; + } + + public Optional getThresholdedImage() { + return Optional.ofNullable(tesseract.TessBaseAPIGetThresholdedImage(getHandle())); + } + + public List getFeaturesForSymbol(BufferedImage symbol) { + if (!lastPix.isPresent()) { + return new LinkedList<>(); + } + + final int padding = 5; + // draw a 5px white padding around the symbol + final BufferedImage symbolWithPadding = new BufferedImage( + symbol.getWidth() + padding + padding, + symbol.getHeight() + padding + padding, + BufferedImage.TYPE_BYTE_BINARY); + + // draw the symbol on the new image + final Graphics2D g2d = symbolWithPadding.createGraphics(); + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, symbolWithPadding.getWidth(), + symbolWithPadding.getHeight()); + g2d.drawImage(symbol, padding, padding, null); + g2d.dispose(); + + // FIXME + if (!controller.getProjectModel().isPresent()) { + return Collections.emptyList(); + } + + final String symbolFile = controller.getProjectModel().get().getProjectDir().resolve( + "symbol.png").toString(); + try { + ImageIO.write(symbolWithPadding, "PNG", new File(symbolFile)); + } catch (IOException e) { + e.printStackTrace(); + return new LinkedList<>(); + } + + try (final lept.PIX pixSymbol = lept.pixRead(symbolFile); + final IntPointer numFeatures = new IntPointer(1); + final IntPointer outlineIndexes = new IntPointer(512); + final BytePointer features = new BytePointer(4 * 512); + final tesseract.INT_FEATURE_STRUCT intFeatures = new tesseract.INT_FEATURE_STRUCT(features)) { + + final tesseract.TBLOB blob = tesseract.TessMakeTBLOB(pixSymbol); + + lept.pixDestroy(pixSymbol); + + tesseract.TessBaseAPIGetFeaturesForBlob(getHandle(), blob, intFeatures, numFeatures, outlineIndexes); + + // make a list of Features3D + final ArrayList featureList = new ArrayList<>(numFeatures.get()); + + for (int i = 0; i < numFeatures.get(); i++) { + final int x = features.get(i * 4) & 0xFF; + final int y = features.get(i * 4 + 1) & 0xFF; + final int theta = features.get(i * 4 + 2) & 0xFF; + final byte cpMisses = features.get(i * 4 + 3); + final int outlineIndex = outlineIndexes.get(i); + + final Feature3D feat = new Feature3D(x, y, theta, cpMisses, outlineIndex); + + featureList.add(feat); + } + + return featureList; + } + } + + public void setVariable(String key, String value) { + variables.put(key, value); + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/PreprocessingWorker.java b/gui/src/main/java/de/vorb/tesseract/gui/work/PreprocessingWorker.java new file mode 100644 index 00000000..eee4912c --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/PreprocessingWorker.java @@ -0,0 +1,56 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.controller.TesseractController; +import de.vorb.tesseract.gui.model.ImageModel; +import de.vorb.tesseract.tools.preprocessing.Preprocessor; +import de.vorb.util.FileNames; + +import javax.imageio.ImageIO; +import javax.swing.SwingWorker; +import java.awt.image.BufferedImage; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; + +public class PreprocessingWorker extends SwingWorker { + private final TesseractController controller; + private final Preprocessor preprocessor; + private final Path sourceFile; + private final Path destinationDir; + + public PreprocessingWorker(TesseractController controller, + Preprocessor preprocessor, Path sourceFile, Path destinationDir) { + this.controller = controller; + this.preprocessor = preprocessor; + this.sourceFile = sourceFile; + this.destinationDir = destinationDir; + } + + @Override + protected ImageModel doInBackground() throws Exception { + Files.createDirectories(destinationDir); + + final Path destFile = destinationDir.resolve(FileNames.replaceExtension( + sourceFile, "png").getFileName()); + + final BufferedImage sourceImg = ImageIO.read(sourceFile.toFile()); + + final BufferedImage preprocessedImg = preprocessor.process(sourceImg); + ImageIO.write(preprocessedImg, "PNG", destFile.toFile()); + + return new ImageModel(sourceFile, sourceImg, destFile, preprocessedImg); + } + + @Override + protected void done() { + try { + controller.setImageModel(Optional.of(get())); + } catch (InterruptedException | ExecutionException + | CancellationException e) { + } finally { + controller.getView().getProgressBar().setIndeterminate(false); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWorker.java b/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWorker.java new file mode 100644 index 00000000..afcfe1cf --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWorker.java @@ -0,0 +1,90 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.controller.TesseractController; +import de.vorb.tesseract.gui.model.ImageModel; +import de.vorb.tesseract.gui.model.PageModel; +import de.vorb.tesseract.gui.view.dialogs.Dialogs; +import de.vorb.tesseract.tools.recognition.PageRecognitionConsumer; +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Page; + +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +public class RecognitionWorker extends SwingWorker { + private final TesseractController controller; + private final ImageModel imageModel; + private final String trainingFile; + private final PageRecognitionProducer producer; + + public RecognitionWorker(TesseractController controller, + ImageModel imageModel, String trainingFile) { + this.controller = controller; + this.imageModel = imageModel; + this.trainingFile = trainingFile; + this.producer = controller.getPageRecognitionProducer(); + } + + @Override + protected PageModel doInBackground() throws Exception { + // set the progress bar state to indeterminate + SwingUtilities.invokeLater(() -> { + controller.setPageModel(Optional.empty()); + controller.getView().getProgressBar().setIndeterminate(true); + }); + + if (trainingFile != null) { + producer.setTrainingFile(trainingFile); + } + + producer.reset(); + + producer.loadImage(imageModel.getPreprocessedFile()); + + final List blocks = new ArrayList<>(1); + + // Get images + final BufferedImage image = imageModel.getPreprocessedImage(); + + producer.recognize(new PageRecognitionConsumer(blocks) { + @Override + public boolean isCancelled() { + return RecognitionWorker.this.isCancelled(); + } + }); + + final Page page = new Page(imageModel.getPreprocessedFile(), + image.getWidth(), image.getHeight(), 300, blocks); + + return new PageModel(imageModel, page, ""); + } + + @Override + protected void done() { + try { + controller.setPageModel(Optional.of(get())); + } catch (ExecutionException e) { + e.printStackTrace(); + + final String message = "The image could not be recognized"; + + controller.setPageModel(Optional.empty()); + + Dialogs.showError(controller.getView(), "Error during recognition", + message); + } catch (InterruptedException e) { + // unexpected: if it is thrown, it is a bug + e.printStackTrace(); + + Dialogs.showError(controller.getView(), "Error during recognition", + "The recognition process has been interrupted unexpectedly."); + } finally { + controller.getView().getProgressBar().setIndeterminate(false); + } + } +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWriter.java b/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWriter.java new file mode 100644 index 00000000..82da1208 --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/RecognitionWriter.java @@ -0,0 +1,10 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.util.Page; + +import java.io.IOException; +import java.io.Writer; + +public interface RecognitionWriter { + void write(Page page, Writer writer) throws IOException; +} diff --git a/gui/src/main/java/de/vorb/tesseract/gui/work/ThumbnailWorker.java b/gui/src/main/java/de/vorb/tesseract/gui/work/ThumbnailWorker.java new file mode 100644 index 00000000..c39a9e8f --- /dev/null +++ b/gui/src/main/java/de/vorb/tesseract/gui/work/ThumbnailWorker.java @@ -0,0 +1,120 @@ +package de.vorb.tesseract.gui.work; + +import de.vorb.tesseract.gui.model.PageThumbnail; +import de.vorb.tesseract.gui.model.ProjectModel; + +import javax.imageio.ImageIO; +import javax.swing.DefaultListModel; +import javax.swing.SwingWorker; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ThumbnailWorker extends + SwingWorker { + private final ProjectModel projectModel; + private final DefaultListModel pages; + + private final Queue tasks = new ConcurrentLinkedQueue<>(); + + public static class Task { + public final int index; + public final PageThumbnail thumbnail; + private boolean cancelled = false; + + public Task(int index, PageThumbnail thumbnail) { + this.index = index; + this.thumbnail = thumbnail; + } + + public void cancel() { + cancelled = true; + } + } + + public ThumbnailWorker(final ProjectModel project, + DefaultListModel pages) { + this.projectModel = project; + this.pages = pages; + } + + public void submitTask(Task task) { + tasks.add(task); + } + + @Override + protected Void doInBackground() throws Exception { + final Path thumbsDir = projectModel.getThumbnailDir(); + + // mkdir -p thumbsDir + Files.createDirectories(thumbsDir); + + while (true) { + if (isCancelled()) { + break; + } + + final Task task = tasks.poll(); + if (task != null) { + if (task.cancelled) { + continue; + } + + final Path imageFile = task.thumbnail.getFile(); + + final Path thumbFile = + thumbsDir.resolve(imageFile.getFileName()); + + final BufferedImage thumb; + + if (Files.isReadable(thumbFile)) { + // if the thumbnail file exists already, load it directly + thumb = ImageIO.read(thumbFile.toFile()); + } else { + // otherwise create a new thumbnail + final BufferedImage img = + ImageIO.read(imageFile.toFile()); + + // calculate width according to aspect ratio + final int width = (int) (100d / img.getHeight() + * img.getWidth()); + + thumb = new BufferedImage(width, 100, + BufferedImage.TYPE_BYTE_GRAY); + + // draw a smoothly scaled version to the thumbnail + final Graphics2D g2d = (Graphics2D) thumb.getGraphics(); + g2d.drawImage(img.getScaledInstance(width, 100, + BufferedImage.SCALE_SMOOTH), 0, 0, null); + g2d.dispose(); + + // release system resources used by this image + img.flush(); + + // write the thumnail to disk + ImageIO.write(thumb, "PNG", thumbFile.toFile()); + } + + publish(new Task(task.index, new PageThumbnail(imageFile, + Optional.of(thumb)))); + } else { + Thread.sleep(500L); + } + } + + return null; + } + + @Override + protected void process(List chunks) { + // update the list model + for (Task chunk : chunks) { + pages.set(chunk.index, chunk.thumbnail); + } + } +} diff --git a/gui/src/main/resources/default_character_equivalences.csv b/gui/src/main/resources/default_character_equivalences.csv new file mode 100644 index 00000000..e48ace7d --- /dev/null +++ b/gui/src/main/resources/default_character_equivalences.csv @@ -0,0 +1,3 @@ +0022,201C; " -> “ +0027,2019; ' -> ’ +017F,0073; langes s -> s diff --git a/gui/src/main/resources/icons/application_tile_horizontal.png b/gui/src/main/resources/icons/application_tile_horizontal.png new file mode 100644 index 00000000..8a1191c3 Binary files /dev/null and b/gui/src/main/resources/icons/application_tile_horizontal.png differ diff --git a/gui/src/main/resources/icons/application_view_icons.png b/gui/src/main/resources/icons/application_view_icons.png new file mode 100644 index 00000000..6a93cdaa Binary files /dev/null and b/gui/src/main/resources/icons/application_view_icons.png differ diff --git a/gui/src/main/resources/icons/book_next.png b/gui/src/main/resources/icons/book_next.png new file mode 100644 index 00000000..ff2ea1ab Binary files /dev/null and b/gui/src/main/resources/icons/book_next.png differ diff --git a/gui/src/main/resources/icons/chart_pie.png b/gui/src/main/resources/icons/chart_pie.png new file mode 100644 index 00000000..fe00fa05 Binary files /dev/null and b/gui/src/main/resources/icons/chart_pie.png differ diff --git a/gui/src/main/resources/icons/cog.png b/gui/src/main/resources/icons/cog.png new file mode 100644 index 00000000..67de2c6c Binary files /dev/null and b/gui/src/main/resources/icons/cog.png differ diff --git a/gui/src/main/resources/icons/contrast.png b/gui/src/main/resources/icons/contrast.png new file mode 100644 index 00000000..adcc0046 Binary files /dev/null and b/gui/src/main/resources/icons/contrast.png differ diff --git a/gui/src/main/resources/icons/cross.png b/gui/src/main/resources/icons/cross.png new file mode 100644 index 00000000..1514d51a Binary files /dev/null and b/gui/src/main/resources/icons/cross.png differ diff --git a/gui/src/main/resources/icons/find.png b/gui/src/main/resources/icons/find.png new file mode 100644 index 00000000..15474796 Binary files /dev/null and b/gui/src/main/resources/icons/find.png differ diff --git a/gui/src/main/resources/icons/folder_explore.png b/gui/src/main/resources/icons/folder_explore.png new file mode 100644 index 00000000..0ba93918 Binary files /dev/null and b/gui/src/main/resources/icons/folder_explore.png differ diff --git a/gui/src/main/resources/icons/information.png b/gui/src/main/resources/icons/information.png new file mode 100644 index 00000000..12cd1aef Binary files /dev/null and b/gui/src/main/resources/icons/information.png differ diff --git a/gui/src/main/resources/icons/magnifier_zoom_in.png b/gui/src/main/resources/icons/magnifier_zoom_in.png new file mode 100644 index 00000000..af4fe074 Binary files /dev/null and b/gui/src/main/resources/icons/magnifier_zoom_in.png differ diff --git a/gui/src/main/resources/icons/magnifier_zoom_out.png b/gui/src/main/resources/icons/magnifier_zoom_out.png new file mode 100644 index 00000000..81f28199 Binary files /dev/null and b/gui/src/main/resources/icons/magnifier_zoom_out.png differ diff --git a/gui/src/main/resources/icons/page_white.png b/gui/src/main/resources/icons/page_white.png new file mode 100644 index 00000000..8b8b1ca0 Binary files /dev/null and b/gui/src/main/resources/icons/page_white.png differ diff --git a/gui/src/main/resources/icons/page_white_stack.png b/gui/src/main/resources/icons/page_white_stack.png new file mode 100644 index 00000000..44084add Binary files /dev/null and b/gui/src/main/resources/icons/page_white_stack.png differ diff --git a/gui/src/main/resources/icons/report.png b/gui/src/main/resources/icons/report.png new file mode 100644 index 00000000..779ad58e Binary files /dev/null and b/gui/src/main/resources/icons/report.png differ diff --git a/gui/src/main/resources/icons/table_edit.png b/gui/src/main/resources/icons/table_edit.png new file mode 100644 index 00000000..bfcb0249 Binary files /dev/null and b/gui/src/main/resources/icons/table_edit.png differ diff --git a/gui/src/main/resources/icons/table_save.png b/gui/src/main/resources/icons/table_save.png new file mode 100644 index 00000000..25b74d18 Binary files /dev/null and b/gui/src/main/resources/icons/table_save.png differ diff --git a/gui/src/main/resources/icons/wand.png b/gui/src/main/resources/icons/wand.png new file mode 100644 index 00000000..44ccbf81 Binary files /dev/null and b/gui/src/main/resources/icons/wand.png differ diff --git a/gui/src/main/resources/icons/zoom_in.png b/gui/src/main/resources/icons/zoom_in.png new file mode 100644 index 00000000..cdf0a52f Binary files /dev/null and b/gui/src/main/resources/icons/zoom_in.png differ diff --git a/gui/src/main/resources/icons/zoom_out.png b/gui/src/main/resources/icons/zoom_out.png new file mode 100644 index 00000000..07bf98a7 Binary files /dev/null and b/gui/src/main/resources/icons/zoom_out.png differ diff --git a/src/main/resources/l10n/labels.properties b/gui/src/main/resources/l10n/labels.properties similarity index 53% rename from src/main/resources/l10n/labels.properties rename to gui/src/main/resources/l10n/labels.properties index 7bf2167b..355471d5 100644 --- a/src/main/resources/l10n/labels.properties +++ b/gui/src/main/resources/l10n/labels.properties @@ -6,7 +6,7 @@ btn_cancel = Cancel frame_title = Tesseract OCR GUI menu_file = File -menu_open_project = Open project +menu_new_project = New project menu_exit = Exit menu_edit = Edit @@ -16,18 +16,27 @@ menu_view = View menu_help = Help menu_about = About +tab_main_preprocessing = Preprocessing tab_main_boxeditor = Box Editor +tab_main_symboloverview = Symbol Overview +tab_main_recognition = Recognition +tab_main_evaluation = Evaluation +tab_main_batchprocessing = Batch Processing project_overview = Project overview: correct_words = Correct words: incorrect_words = Incorrect words: total_words = Total words: +# Preprocessing + +btn_update_preview = Update preview + # About dialog about_title = About this software -about_message = 2014 Paul Vorbach\nE-mail: paul@vorba.ch\nWebsite: http://paul.vorba.ch/ +about_message = 2014-2016 Paul Vorbach\nE-mail: paul@vorba.ch\nWebsite: https://paul.vorba.ch/ # Open project -open_dialog_title = Open Project +new_project_dialog_title = New Project scan_dir = Scan directory: hocr_dir = HOCR directory: diff --git a/src/main/resources/logos/logo_16.png b/gui/src/main/resources/logos/logo_16.png similarity index 100% rename from src/main/resources/logos/logo_16.png rename to gui/src/main/resources/logos/logo_16.png diff --git a/src/main/resources/logos/logo_256.png b/gui/src/main/resources/logos/logo_256.png similarity index 100% rename from src/main/resources/logos/logo_256.png rename to gui/src/main/resources/logos/logo_256.png diff --git a/src/main/resources/logos/logo_96.png b/gui/src/main/resources/logos/logo_96.png similarity index 100% rename from src/main/resources/logos/logo_96.png rename to gui/src/main/resources/logos/logo_96.png diff --git a/gui/src/main/resources/page_loading.png b/gui/src/main/resources/page_loading.png new file mode 100644 index 00000000..22a3a253 Binary files /dev/null and b/gui/src/main/resources/page_loading.png differ diff --git a/ocrevalUAtion/AUTHORS b/ocrevalUAtion/AUTHORS new file mode 100644 index 00000000..adacc4a7 --- /dev/null +++ b/ocrevalUAtion/AUTHORS @@ -0,0 +1 @@ +Rafael C. Carrasco (carrasco@ua.es) \ No newline at end of file diff --git a/ocrevalUAtion/README.md b/ocrevalUAtion/README.md new file mode 100644 index 00000000..bcf32a8a --- /dev/null +++ b/ocrevalUAtion/README.md @@ -0,0 +1,25 @@ +ocrevalUAtion [![Build Status](https://secure.travis-ci.org/impactcentre/ocrevalUAtion.png?branch=master)](http://travis-ci.org/impactcentre/ocrevalUAtion) +============= + +This set of classes provides basic support to perform the comparison of +two text files: a reference file (a ground-truth document) and a the output from an OCR engine (a text file). + +Options for specific behavior include: ignore case, ignore diacritics, +ignore punctuation, ignore stop-words, Unicode and user-defined equivalences between characters. + +It can be used with the graphic user interface (GUI) provided, in addition to command line interface usage. + +Supported input formats include: plain text, FineReader 10 XML, PAGE XML, ALTO XML and hOCR HTML. + +The output generates a report with statistics (including CER and WER error rates) +and a table with the parallell input texts where the differences are highlighted. + +A gentle introduction to OCR evaluation and to this tool can be found at https://sites.google.com/site/textdigitisation/ + +You can download the latest release from [here](https://bintray.com/impactocr/maven/ocrevalUAtion). + +Instructions on how to use ocrevalUAtion can be found in the [wiki](https://github.com/impactcentre/ocrevalUAtion/wiki). + + + + diff --git a/ocrevalUAtion/pom.xml b/ocrevalUAtion/pom.xml new file mode 100644 index 00000000..45b5a58e --- /dev/null +++ b/ocrevalUAtion/pom.xml @@ -0,0 +1,270 @@ + + 4.0.0 + + de.vorb.tesseract + tesseract4java + 0.3.0-SNAPSHOT + + ocrevalUAtion + ocrevalUAtion + jar + OCR Evaluation Tool + + IMPACT Centre of Competence + http://www.digitisation.eu/ + + 2009 + + scm:git:https://github.com/impactcentre/ocrevalUAtion.git + scm:git:git@github.com:impactcentre/ocrevalUAtion.git + https://github.com/impactcentre/ocrevalUAtion + HEAD + + + + GNU General Public License 2.0 + http://www.gnu.org/licenses/gpl-2.0.html + + + + UTF-8 + 1.7 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + true + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + ${basedir}/api + api + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + eu.digitisation.input.GUI + + + + + + maven-assembly-plugin + 2.4 + + ocrevaluation + + jar-with-dependencies + + false + + + true + eu.digitisation.input.GUI + + + + + + make-assembly + package + + single + + + + + + maven-resources-plugin + 2.6 + + + copy-resources + validate + + copy-resources + + + ${basedir}/target + + + . + true + + userProperties.xml + + + + + + + + + + + maven-deploy-plugin + 2.8.1 + + internal.repo::default::file://${project.build.directory}/mvn-repo + + + + + + + + internal.repo + Temporary Staging Repository + file://${project.build.directory}/mvn-repo + + + + + junit + junit + 4.13.1 + test + + + + javax.media.jai + com.springsource.javax.media.jai.core + 1.1.3 + + + javax.media.jai + com.springsource.javax.media.jai.codec + 1.1.3 + + + org.jsoup + jsoup + 1.14.2 + + + org.apache.tika + tika-parsers + 1.4 + jar + + + org.apache.geronimo.specs + geronimo-stax-api_1.0_spec + + + org.gagravarr + vorbis-java-core + + + xml-apis + xml-apis + + + rome + rome + + + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 + + + + + + com.springsource.repository.bundles.external + SpringSource Enterprise Bundle Repository - External Bundle Releases + https://repository.springsource.com/maven/bundles/external + + + diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/File2Text.java b/ocrevalUAtion/src/main/java/eu/digitisation/File2Text.java new file mode 100644 index 00000000..e55a5d70 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/File2Text.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation; + +import eu.digitisation.input.SchemaLocationException; +import eu.digitisation.input.WarningException; +import eu.digitisation.text.CharFilter; +import eu.digitisation.text.Text; +import java.io.File; + +/** + * + * @author R.C.C + */ +public class File2Text { + + public static void main(String[] args) throws WarningException, + SchemaLocationException { + if (args.length > 0) { + File file = new File(args[0]); + CharFilter filter = null; + if (args.length > 1) { + filter = new CharFilter(new File(args[1])); + } + Text content = new Text(file); + System.out.println(content.toString(filter)); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/Main.java b/ocrevalUAtion/src/main/java/eu/digitisation/Main.java new file mode 100644 index 00000000..343b7312 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/Main.java @@ -0,0 +1,90 @@ +package eu.digitisation; + +import eu.digitisation.input.Batch; +import eu.digitisation.input.Parameters; +import eu.digitisation.input.SchemaLocationException; +import eu.digitisation.input.WarningException; +import eu.digitisation.log.Messages; +import eu.digitisation.output.Report; +import java.io.File; +import java.io.InvalidObjectException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Main class for ocrevalUAtion: version 0.92 + */ +public class Main { + + static final String helpMsg = "Usage:\t" + + "ocrevalUAtion -gt file1" + + "-ocr file2" + + "[-e encoding]" + + " [-o output_file_or_dir ] [-r equivalences_file]" + + " [-c] [-ic] [-id] [-ip]"; + + private static void exit_gracefully() { + System.err.println(helpMsg); + System.exit(0); + } + + /** + * @param args the command line arguments + * @throws eu.digitisation.input.WarningException + */ + public static void main(String[] args) throws WarningException { + Parameters pars = new Parameters(); + File workingDirectory; + + // Read parameters (String switch needs Java 1.7 or later) + for (int n = 0; n < args.length; ++n) { + String arg = args[n]; + if (arg.equals("-h")) { + exit_gracefully(); + } else if (arg.equals("-gt")) { + pars.gtfile.setValue(new File(args[++n])); + } else if (arg.equals("-e")) { + pars.encoding.setValue(args[++n]); + } else if (arg.equals("-ocr")) { + pars.ocrfile.setValue(new File(args[++n])); + } else if (arg.equals("-r")) { + pars.eqfile.setValue(new File(args[++n])); + } else if (arg.equals("-o")) { + pars.outfile.setValue(new File(args[++n])); + } else if (arg.equals("-c")) { + pars.compatibility.setValue(true); + } else if (arg.equals("-ic")) { + pars.ignoreCase.setValue(true); + } else if (arg.equals("-id")) { + pars.ignoreDiacritics.setValue(true); + } else if (arg.equals("-ip")) { + pars.ignorePunctuation.setValue(true); + } else { + System.err.println("Unrecognized option " + arg); + exit_gracefully(); + } + } + + if (pars.gtfile.getValue() == null || pars.ocrfile.getValue() == null) { + System.err.println("Not enough arguments"); + exit_gracefully(); + } + + if (pars.outfile.getValue() == null) { + String name = pars.ocrfile.getValue().getName().replaceAll("\\.\\w+", "") + + "_report.html"; + pars.outfile.setValue(new File(pars.ocrfile.getValue().getParent(), name)); + } + + try { + Batch batch = new Batch(pars.gtfile.getValue(), pars.ocrfile.getValue()); + Report report = new Report(batch, pars); + report.write(pars.outfile.getValue()); + } catch (InvalidObjectException ex) { + Messages.info(Main.class.getName() + ": " + ex); + } catch (SchemaLocationException ex) { + Messages.info(Main.class.getName() + ": " + ex); + } + + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/Aligner.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/Aligner.java new file mode 100644 index 00000000..125ee5cc --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/Aligner.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.text.CharMap; +import eu.digitisation.text.Text; +import eu.digitisation.xml.DocumentBuilder; +import java.io.File; +import org.w3c.dom.Element; + +/** + * Alignments between 2 texts (output in XHTML format) + * + * @author R.C.C + */ +public class Aligner { + + // style for unaligned segments + final static String uStyle = "background-color:aquamarine"; + // style for highlight replacement in parallel text + final static String twinStyle = ""; + + /** + * @return 3-wise minimum. + */ + private static int min(int x, int y, int z) { + return Math.min(x, Math.min(y, z)); + } + + /** + * Compute the table of basic edit operations needed to transform first into + * second + * + * @param first + * source string + * @param second + * target string + * @return the table of minimal basic edit operations needed to transform + * first into second + */ + private static EditTable alignTab(String first, String second) { + int l1; // length of first + int l2; // length of second + int[][] A; // distance table + EditTable B; // edit operations + + // intialize + l1 = first.length(); + l2 = second.length(); + A = new int[2][second.length() + 1]; + B = new EditTable(first.length() + 1, second.length() + 1); + // Compute first row + A[0][0] = 0; + B.set(0, 0, EdOp.KEEP); + for (int j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + B.set(0, j, EdOp.INSERT); + } + + // Compute other rows + for (int i = 1; i <= first.length(); ++i) { + char c1 = first.charAt(l1 - i); + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + B.set(i, 0, EdOp.DELETE); + for (int j = 1; j <= second.length(); ++j) { + char c2 = second.charAt(l2 - j); + + if (c1 == c2) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + B.set(i, j, EdOp.KEEP); + } else if (Character.isSpaceChar(c1) + ^ Character.isSpaceChar(c2)) { + // No alignment between blank and not-blank + if (A[(i - 1) % 2][j] < A[i % 2][j - 1]) { + A[i % 2][j] = A[(i - 1) % 2][j] + 1; + B.set(i, j, EdOp.DELETE); + } else { + A[i % 2][j] = A[i % 2][j - 1] + 1; + B.set(i, j, EdOp.INSERT); + } + } else { + A[i % 2][j] = min(A[(i - 1) % 2][j] + 1, + A[i % 2][j - 1] + 1, + A[(i - 1) % 2][j - 1] + 1); + if (A[i % 2][j] == A[(i - 1) % 2][j] + 1) { + B.set(i, j, EdOp.DELETE); + } else if (A[i % 2][j] == A[i % 2][j - 1] + 1) { + B.set(i, j, EdOp.INSERT); + } else { + B.set(i, j, EdOp.SUBSTITUTE); + } + } + } + } + return B; + } + + /** + * A minimal sequence of edit operations transforming the first string into + * the second + * + * @param first + * the first string + * @param second + * the second string + * @return a minimal sequence of edit operations transforming the first + * string into the second + */ + public static EditSequence path(String first, String second) { + return alignTab(first, second).path(); + } + + /** + * Shows text alignment based on a pseudo-Levenshtein distance where + * white-spaces are not allowed to be replaced with text or vice-versa + * + * @param header1 + * first text title for table head + * @param header2 + * second text title for table head + * @param first + * the first text + * @param second + * the second text + * @param map + * a CharMap for character equivalences + * @return a table in XHTML format showing the alignments + */ + public static Element alignmentMap(String header1, String header2, + String first, String second, CharMap map) { + EditTable B = (map == null) + ? alignTab(first, second) + : alignTab(map.normalForm(first), map.normalForm(second)); + DocumentBuilder builder = new DocumentBuilder("table"); + Element table = builder.root(); + Element row; + Element cell1; + Element cell2; + int l1; + int l2; + int len; + int i; + int j; + String s1; + String s2; + + // features + table.setAttribute("border", "1"); + // content + row = builder.addElement("tr"); + cell1 = builder.addElement(row, "td"); + cell2 = builder.addElement(row, "td"); + cell1.setAttribute("width", "50%"); + cell2.setAttribute("width", "50%"); + cell1.setAttribute("align", "center"); + cell2.setAttribute("align", "center"); + builder.addTextElement(cell1, "h3", header1); + builder.addTextElement(cell2, "h3", header2); + row = builder.addElement("tr"); + cell1 = builder.addElement(row, "td"); + cell2 = builder.addElement(row, "td"); + + l1 = first.length(); + l2 = second.length(); + i = l1; + j = l2; + while (i > 0 && j > 0) { + switch (B.get(i, j)) { + case KEEP: + len = 1; + while (len < i && len < j + && B.get(i - len, j - len) == EdOp.KEEP) { + ++len; + } + s1 = first.substring(l1 - i, l1 - i + len); + s2 = second.substring(l2 - j, l2 - j + len); + builder.addText(cell1, s1); + builder.addText(cell2, s2); + i -= len; + j -= len; + break; + case DELETE: + len = 1; + while (len < i && B.get(i - len, j) == EdOp.DELETE) { + ++len; + } + s1 = first.substring(l1 - i, l1 - i + len); + builder.addTextElement(cell1, "font", s1) + .setAttribute("style", uStyle); + i -= len; + break; + case INSERT: + len = 1; + + while (len < j && B.get(i, j - len) == EdOp.INSERT) { + ++len; + } + s2 = second.substring(l2 - j, l2 - j + len); + builder.addTextElement(cell2, "font", s2) + .setAttribute("style", uStyle); + j -= len; + break; + case SUBSTITUTE: + len = 1; + while (len < i && len < j + && B.get(i - len, j - len) == EdOp.SUBSTITUTE) { + ++len; + } + s1 = first.substring(l1 - i, l1 - i + len); + s2 = second.substring(l2 - j, l2 - j + len); + Element span1 = builder.addElement(cell1, "span"); + Element span2 = builder.addElement(cell2, "span"); + String id1 = "l" + i + "." + j; + String id2 = "r" + i + "." + j; + span1.setAttribute("title", s2); + span2.setAttribute("title", s1); + span1.setAttribute("id", id1); + span2.setAttribute("id", id2); + span1.setAttribute("onmouseover", + "document.getElementById('" + + id2 + "').style.background='greenyellow'"); + span2.setAttribute("onmouseover", + "document.getElementById('" + + id1 + "').style.background='greenyellow'"); + span1.setAttribute("onmouseout", + "document.getElementById('" + + id2 + "').style.background='none'"); + span2.setAttribute("onmouseout", + "document.getElementById('" + + id1 + "').style.background='none'"); + builder.addTextElement(span1, "font", s1) + .setAttribute("color", "red"); + builder.addTextElement(span2, "font", s2) + .setAttribute("color", "red"); + i -= len; + j -= len; + break; + } + } + if (i > 0) { + s1 = first.substring(l1 - i, l1); + builder.addTextElement(cell1, "font", s1) + .setAttribute("style", uStyle); + + } + if (j > 0) { + s2 = second.substring(l2 - j, l2); + builder.addTextElement(cell2, "font", s2) + .setAttribute("style", uStyle); + } + return builder.document().getDocumentElement(); + } + + /** + * Shows text alignment based on a pseudo-Levenshtein distance where + * white-spaces are not allowed to be replaced with text or vice-versa + * + * @param header1 + * first text title for table head + * @param header2 + * second text title for table head + * @param first + * the first text + * @param second + * the second text + * @return a table in XHTML format showing the alignments + */ + public static Element alignmentMap(String header1, String header2, + String first, String second) { + return alignmentMap(header1, header2, first, second, null); + } + + /** + * Shows text alignment based on a pseudo-Levenshtein distance where + * white-spaces are not allowed to be replaced with text or vice-versa + * + * @param header1 + * first text title for table head + * @param header2 + * second text title for table head + * @param first + * the first text + * @param second + * the second text + * @param w + * the weighs associated to basic edit operations + * @return a table in XHTML format showing the alignments + */ + public static Element bitext(String header1, String header2, + String first, String second, EdOpWeight w, EditSequence edition) { + DocumentBuilder builder = new DocumentBuilder("table"); + Element table = builder.root(); + Element row; + Element cell1; + Element cell2; + int l1; + int l2; + int len; + int i; + int j; + String s1; + String s2; + + // features + table.setAttribute("border", "1"); + // content + row = builder.addElement("tr"); + cell1 = builder.addElement(row, "td"); + cell2 = builder.addElement(row, "td"); + cell1.setAttribute("width", "50%"); + cell2.setAttribute("width", "50%"); + cell1.setAttribute("align", "center"); + cell2.setAttribute("align", "center"); + builder.addTextElement(cell1, "h3", header1 + " (Transcription)"); + builder.addTextElement(cell2, "h3", header2 + " (OCR Result)"); + row = builder.addElement("tr"); + cell1 = builder.addElement(row, "td"); + cell2 = builder.addElement(row, "td"); + + l1 = first.length(); + l2 = second.length(); + i = 0; + j = 0; + len = 0; + + for (int n = 0; n < edition.size(); n += len) { + EdOp op = edition.get(n); + + // free rides first + if (op == EdOp.DELETE && w.del(first.charAt(i)) == 0) { + builder.addText(cell1, first.substring(i, i + 1)); + ++i; + len = 1; + } else if (op == EdOp.INSERT && w.ins(second.charAt(j)) == 0) { + builder.addText(cell2, second.substring(j, j + 1)); + ++j; + len = 1; + } else if (op == EdOp.SUBSTITUTE + && w.sub(first.charAt(i), second.charAt(j)) == 0) { + builder.addText(cell1, first.substring(i, i + 1)); + builder.addText(cell2, second.substring(j, j + 1)); + ++i; + ++j; + len = 1; + } else { + switch (op) { + case KEEP: + len = 1; + while (i + len < l1 && j + len < l2 + && edition.get(n + len) == EdOp.KEEP) { + ++len; + } + s1 = first.substring(i, i + len); + s2 = second.substring(j, j + len); + builder.addText(cell1, s1); + builder.addText(cell2, s2); + i += len; + j += len; + break; + case DELETE: + len = 1; + while (i + len < l1 + && edition.get(n + len) == EdOp.DELETE + && w.del(first.charAt(i + len)) > 0) { + ++len; + } + s1 = first.substring(i, i + len); + builder.addTextElement(cell1, "font", s1) + .setAttribute("style", uStyle); + i += len; + break; + + case INSERT: + len = 1; + while (j + len < l2 + && edition.get(n + len) == EdOp.INSERT + && w.ins(second.charAt(j + len)) > 0) { + ++len; + } + s2 = second.substring(j, j + len); + builder.addTextElement(cell2, "font", s2) + .setAttribute("style", uStyle); + j += len; + break; + case SUBSTITUTE: + len = 1; + while (i + len < l1 + && j + len < l2 + && edition.get(n + len) == EdOp.SUBSTITUTE + && w.sub(first.charAt(i + len), + second.charAt(j + len)) > 0) { + ++len; + } + s1 = first.substring(i, i + len); + s2 = second.substring(j, j + len); + Element span1 = builder.addElement(cell1, "span"); + Element span2 = builder.addElement(cell2, "span"); + String id1 = "l" + i + "." + j; + String id2 = "r" + i + "." + j; + span1.setAttribute("title", s2); + span2.setAttribute("title", s1); + span1.setAttribute("id", id1); + span2.setAttribute("id", id2); + span1.setAttribute("onmouseover", + "document.getElementById('" + + id2 + "').style.background='greenyellow'"); + span2.setAttribute("onmouseover", + "document.getElementById('" + + id1 + "').style.background='greenyellow'"); + span1.setAttribute("onmouseout", + "document.getElementById('" + + id2 + "').style.background='none'"); + span2.setAttribute("onmouseout", + "document.getElementById('" + + id1 + "').style.background='none'"); + builder.addTextElement(span1, "font", s1) + .setAttribute("color", "red"); + builder.addTextElement(span2, "font", s2) + .setAttribute("color", "red"); + i += len; + j += len; + break; + } + } + } + return builder.document().getDocumentElement(); + } + + public static void main(String[] args) + throws Exception { + File f1 = new File(args[0]); + File f2 = new File(args[1]); + File ofile = new File("/tmp/out.html"); + + String s1 = new Text(f1).toString(); + String s2 = new Text(f2).toString(); + EdOpWeight w = new OcrOpWeight(); + EditSequence eds = new EditSequence(s1, s2, w); + DocumentBuilder builder = new DocumentBuilder("html"); + Element body = builder.addElement("body"); + Element alitab = Aligner.bitext("s1", "s2", s1, s2, w, eds); + builder.addElement(body, alitab); + builder.write(ofile); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/ArrayEditDistance.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/ArrayEditDistance.java new file mode 100644 index 00000000..50d84339 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/ArrayEditDistance.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +/** + * Provides a basic implementations of some popular edit distance methods + * applied to arrays of objects (currently, Levenshtein and indel). This + * implementation can accelerates the computation of distances if, for example, + * text is handled as a sequence of (Integer) word codes. + * + * @version 2011.03.10 + * @param type of content + */ +public class ArrayEditDistance { + + /** + * @return 3-wise minimum. + */ + private static int min(int x, int y, int z) { + return Math.min(x, Math.min(y, z)); + } + + /** + * @param the type of object in the array + * @param first the first string. + * @param second the second string. + * @return the indel distance between first and second. + */ + public static int indel(Type[] first, Type[] second) { + int i, j; + int[][] A = new int[first.length + 1][second.length + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length; ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length; ++i) { + A[i][0] = A[i - 1][0] + 1; + for (j = 1; j <= second.length; ++j) { + if (first[i - 1].equals(second[j - 1])) { + A[i][j] = A[i - 1][j - 1]; + } else { + A[i][j] = Math.min(A[i - 1][j] + 1, A[i][j - 1] + 1); + } + } + } + return A[first.length][second.length]; + } + + /** + * @param the type of object in the array + * @param first the first string. + * @param second the second string. + * @return the Levenshtein distance between first and second. + */ + public static int levenshtein(Type[] first, Type[] second) { + int i, j; + int[][] A; + + // intialize + A = new int[first.length + 1][second.length + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length; ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length; ++i) { + A[i][0] = A[i - 1][0] + 1; + for (j = 1; j <= second.length; ++j) { + if (first[i - 1] == second[j - 1]) { + A[i][j] = A[i - 1][j - 1]; + } else { + A[i][j] = min(A[i - 1][j] + 1, A[i][j - 1] + 1, + A[i - 1][j - 1] + 1); + } + } + } + return A[first.length][second.length]; + } + + /** + * @param + * @param first the first array. + * @param second the second array. + * @return the Damerau-Levenshtein distance between first and second. + */ + public static int DL(Type[] first, Type[] second) { + int i, j; + int[][] A; + + // intialize + A = new int[3][second.length+ 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length; ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length; ++i) { + A[i % 3][0] = A[(i - 1) % 3][0] + 1; + for (j = 1; j <= second.length; ++j) { + if (first[i - 1] == second[j - 1]) { + A[i % 3][j] = A[(i - 1) % 3][j - 1]; + } else { + if (i > 1 && j > 1 + && first[i - 1] == second[j - 2] + && first[i - 2] == second[j - 1]) { + A[i % 3][j] = min(A[(i - 1) % 3][j] + 1, + A[i % 3][j - 1] + 1, + A[(i - 2) % 3][j - 2] + 1); + } else { + A[i % 3][j] = min(A[(i - 1) % 3][j] + 1, + A[i % 3][j - 1] + 1, + A[(i - 1) % 3][j - 1] + 1); + } + } + } + } + return A[first.length % 3][second.length]; + } + + /** + * + * @param + * @param first the first array. + * @param second the second array. + * @param type the type of distance to be computed + * @return the distance between first and second (defaults to Levenshtein) + */ + public static int distance(Type[] first, Type[] second, + EditDistanceType type) { + switch (type) { + case INDEL: + return indel(first, second); + case LEVENSHTEIN: + return levenshtein(first, second); + case DAMERAU_LEVENSHTEIN: + return DL(first, second); + default: + return levenshtein(first, second); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOp.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOp.java new file mode 100644 index 00000000..c98c21e8 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOp.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +/** + * + * Basic edit operations on a single character + * + * @author R.C.C. + */ +@SuppressWarnings("javadoc") +public enum EdOp { + + KEEP, INSERT, SUBSTITUTE, DELETE; + + @Override + public String toString() { + switch (this) { + case KEEP: + return "K"; + case INSERT: + return "I"; + case SUBSTITUTE: + return "S"; + case DELETE: + return "D"; + default: + return null; + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOpWeight.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOpWeight.java new file mode 100644 index 00000000..7899b452 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EdOpWeight.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package eu.digitisation.distance; + +/** + * Integer weights for basic edit operations. + * @author R.C.C. + */ +public interface EdOpWeight { + public int sub(char c1, char c2); + public int ins(char c); + public int del(char c); +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistance.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistance.java new file mode 100644 index 00000000..e2c5af02 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistance.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.document.TokenArray; +import eu.digitisation.math.MinimalPerfectHash; +import eu.digitisation.text.Text; +import eu.digitisation.text.WordSet; +import java.io.File; + +/** + * Provides linear time implementations of some popular edit distance methods + * operating on strings + * + * @version 2014.01.25 + */ +public class EditDistance { + + /** + * @param s1 the first string. + * @param s2 the second string. + * @param w weights for basic edit operations + * @param chunkLen the length of the chunks analyzed at every step (must be + * strictly greater than 1) + * @return the approximate (linear time) Levenshtein distance between first + * and second. + */ + public static int charDistance(String s1, String s2, EdOpWeight w, int chunkLen) { + EditSequence seq = new EditSequence(s1, s2, w, chunkLen); + return seq.cost(s1, s2, w); + } + + /** + * @param s1 the first string. + * @param s2 the second string. + * @param chunkLen the length of the chunks analyzed at every step (must be + * strictly greater than 1) + * @return the length (number of words) of first, the length (number of + * words) of second, and the approximate (linear time) word-based + * Levenshtein distance between first and second. + */ + public static int[] wordDistance(String s1, String s2, int chunkLen) { + MinimalPerfectHash mph = new MinimalPerfectHash(true); // case sensitive + TokenArray a1 = new TokenArray(mph, s1); + TokenArray a2 = new TokenArray(mph, s2); + EditSequence seq = new EditSequence(a1, a2, chunkLen); + return new int[]{a1.length(), a2.length(), seq.length()}; + } + + /** + * @param s1 the first string. + * @param s2 the second string. + * @param stopwords a set of stop-words + * @param chunkLen the length of the chunks analyzed at every step (must be + * strictly greater than 1) + * @return the length (number of words) of first, the length (number of + * words) of second, and the approximate (linear time) word-based + * Levenshtein distance between first and second. Whenever a stop-word in + * the first string is aligned (substituted) with a different target word in + * the second string or with no string at all (deleted), this difference + * does not contribute to the distance + */ + public static int[] wordDistance(String s1, String s2, + WordSet stopwords, int chunkLen) { + MinimalPerfectHash mph = new MinimalPerfectHash(true); // case sensitive + TokenArray a1 = new TokenArray(mph, s1); + TokenArray a2 = new TokenArray(mph, s2); + EditSequence seq = new EditSequence(a1, a2, chunkLen); + int d = 0; + int n1 = 0; + int n2 = 0; + for (EdOp op : seq.ops) { + String word = a1.wordAt(n1); + if (op != EdOp.KEEP && !stopwords.contains(word)) { + ++d; + } + if (op != EdOp.INSERT) { + ++n1; + } + if (op != EdOp.DELETE) { + ++n2; + } + } + return new int[]{a1.length(), a2.length(), d}; + } + + /** + * + * @param first the first string. + * @param second the second string. + * @param chunkLen the length of the chunks analyzed at every step + * @param type the type of distance to be computed + * @return the distance between first and second (defaults to Levenshtein) + * @throws java.lang.NoSuchMethodException + */ + public static int distance(String first, String second, + int chunkLen, EditDistanceType type) + throws NoSuchMethodException { + switch (type) { + case OCR_CHAR: + EdOpWeight w = new OcrOpWeight(); + return charDistance(first, second, w, chunkLen); + case OCR_WORD: + return wordDistance(first, second, chunkLen)[2]; + default: + throw new java.lang.NoSuchMethodException(type + + " distance still to be implemented"); + + } + } + + public static void main(String[] args) throws Exception { + File f1 = new File(args[0]); + File f2 = new File(args[1]); + int len = Integer.parseInt(args[2]); + String s1 = new Text(f1).toString(); + String s2 = new Text(f2).toString(); + int d = EditDistance.distance(s1, s2, len, EditDistanceType.OCR_CHAR); + System.out.println(d); + d = EditDistance.distance(s1, s2, len, EditDistanceType.OCR_WORD); + System.out.println(d); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistanceType.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistanceType.java new file mode 100644 index 00000000..2467c984 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditDistanceType.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +/** + * Basic edit distance variants. + * @author R.C.C + */ +public enum EditDistanceType { + + INDEL, LEVENSHTEIN, DAMERAU_LEVENSHTEIN, OCR_CHAR, OCR_WORD; +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditSequence.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditSequence.java new file mode 100644 index 00000000..926066b8 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditSequence.java @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.document.TokenArray; +import eu.digitisation.log.Messages; +import eu.digitisation.math.BiCounter; +import eu.digitisation.text.Text; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An arbitrary length sequence of basic edit operations (keep, insert, + * substitute, delete). + * + * @author R.C.C. + */ +public class EditSequence { + + List ops; // the list of edit operations + int numOps; // the number of non-trivial (KEEP) operations + int length1; // the length of the first string (number of non-INSERT operations in the list) + int length2; // the lenth of the second string (number of non-DELETE operations in the list) + + /** + * Constructs an empty list with an initial capacity of ten. + */ + public EditSequence() { + ops = new ArrayList(); + } + + /** + * Create an EditPAth with the specified initial capacity + * + * @param initialCapacity + */ + public EditSequence(int initialCapacity) { + ops = new ArrayList(initialCapacity); + } + + /** + * + * @param pos a position in the sequence + * @return the basic edit operation in the sequence which is at the + * specified position + */ + public EdOp get(int pos) { + return ops.get(pos); + } + + /** + * Add an operation to the sequence + * + * @param op an edit operation + */ + public final void add(EdOp op) { + ops.add(op); + switch (op) { + case KEEP: + ++length1; + ++length2; + break; + case INSERT: + ++length2; + ++numOps; + break; + case SUBSTITUTE: + ++length1; + ++length2; + ++numOps; + break; + case DELETE: + ++length1; + ++numOps; + break; + } + } + + /** + * Add an operation to the sequence + * + * @param other another sequence of edit operations + */ + public final void append(EditSequence other) { + for (EdOp op : other.ops) { + this.add(op); + } + } + + /** + * Build a new path containing only a prefix of the sequence + * + * @param len the length of the new sequence + * @return the path truncated to the required length + */ + public EditSequence head(int len) { + EditSequence path = new EditSequence(); + for (int n = 0; n < len; ++n) { + path.add(ops.get(n)); + } + return path; + } + + /** + * The size of the list + * + * @return the number of basic edit operations in the sequence + */ + public int size() { + return ops.size(); + } + + /** + * + * @return the number of non-trivial (KEEP) edit operations in this sequence + */ + public int length() { + return numOps; + } + + /** + * The length of the first string + * + * @return the number of edit operations in the sequence involving the first + * string (all but DELETE) + */ + public final int shift1() { + return length1; + } + + /** + * The length of the second string + * + * @return the number of edit operations in the sequence involving the + * second string (all but INSERT) + */ + public final int shift2() { + return length2; + } + + /** + * String representation + * + * @return a string representing the EditSequence + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (EdOp op : ops) { + builder.append(op); + } + return builder.toString(); + } + + /** + * Build the EditSequence for a pair of strings + * + * @param first the first string + * @param second the second string + * @param w the weights applied to basic edit operations + */ + public EditSequence(String first, String second, EdOpWeight w) { + int l1; // length of first + int l2; // length of second + int[][] A; // distance table + EditTable B; // edit operations + + // intialize + l1 = first.length(); + l2 = second.length(); + A = new int[2][second.length() + 1]; + B = new EditTable(first.length() + 1, second.length() + 1); + // Compute first row + A[0][0] = 0; + B.set(0, 0, EdOp.KEEP); + for (int j = 1; j <= second.length(); ++j) { + char c2 = second.charAt(j - 1); + A[0][j] = A[0][j - 1] + w.ins(c2); + B.set(0, j, EdOp.INSERT); + } + + // Compute other rows + for (int i = 1; i <= first.length(); ++i) { + char c1 = first.charAt(i - 1); + A[i % 2][0] = A[(i - 1) % 2][0] + w.del(c1); + B.set(i, 0, EdOp.DELETE); + for (int j = 1; j <= second.length(); ++j) { + char c2 = second.charAt(j - 1); + + if (c1 == c2) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + B.set(i, j, EdOp.KEEP); + } else { + A[i % 2][j] = Math.min(A[(i - 1) % 2][j - 1] + w.sub(c1, c2), + Math.min(A[i % 2][j - 1] + w.ins(c2), + A[(i - 1) % 2][j] + w.del(c1))); + + if (A[i % 2][j] == A[i % 2][j - 1] + w.ins(c2)) { + B.set(i, j, EdOp.INSERT); + } else if (A[i % 2][j] == A[(i - 1) % 2][j] + w.del(c1)) { + B.set(i, j, EdOp.DELETE); + } else { + B.set(i, j, EdOp.SUBSTITUTE); + } + } + } + } + + // extract sequence of edit operations + int i = B.width - 1; + int j = B.height - 1; + + ops = new ArrayList(); + + while (i > 0 || j > 0) { + EdOp e = null; + try { + e = B.get(i, j); + } catch (Exception ex) { + Messages.severe(i + "," + j); + Messages.severe(B.toString()); + } + switch (e) { + case INSERT: + --j; + break; + case DELETE: + --i; + break; + default: + --i; + --j; + break; + } + add(e); + } + if (i != 0 + || j != 0 + || length1 != first.length() + || length2 != second.length()) { + throw new java.lang.IllegalArgumentException("Unvalid EditTable"); + } else { + Collections.reverse(ops); + } + } + + /** + * Linear-time approximation to the construction of the EditSequence for a + * pair of strings. The complexity gets reduced by splitting the strings + * into smaller, overlapping, chunks. + * + * @param s1 the first string + * @param s2 the second string + * @param w the weights applied to basic edit operations + * @param chunkLen the length of the chunks in which the strings are split + * + */ + public EditSequence(String s1, String s2, EdOpWeight w, int chunkLen) { + int len1 = s1.length(); + int len2 = s2.length(); + + if (chunkLen < 2) { + throw new IllegalArgumentException("chunkLen mut be greater than 1"); + } + + ops = new ArrayList(); + while (shift1() < len1 || shift2() < len2) { + int high1 = Math.min(shift1() + chunkLen, len1); + int high2 = Math.min(shift2() + chunkLen, len2); + String sub1 = s1.substring(shift1(), high1); + String sub2 = s2.substring(shift2(), high2); + EditSequence subseq = new EditSequence(sub1, sub2, w); + EditSequence head = (high1 < len1 || high2 < len2) + ? subseq.head(subseq.size() / 2) + : subseq; + + append(head); + if (len1 > 10 * chunkLen) { + int frac = (100 * length1) / len1; + Messages.info(frac + "% of file processed"); + } + } + } + + /** + * Build the EditSequence for a pair of TokenArrays + * + * @param first the first TokenArray + * @param second the second TokenArray + */ + public EditSequence(TokenArray first, TokenArray second) { + int l1; // length of first + int l2; // length of second + int[][] A; // distance table + EditTable B; // edit operations + + // intialize + l1 = first.length(); + l2 = second.length(); + A = new int[2][second.length() + 1]; + B = new EditTable(first.length() + 1, second.length() + 1); + // Compute first row + A[0][0] = 0; + B.set(0, 0, EdOp.KEEP); + for (int j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + B.set(0, j, EdOp.INSERT); + } + + // Compute other rows + for (int i = 1; i <= first.length(); ++i) { + int n1 = first.tokenAt(i - 1); + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + B.set(i, 0, EdOp.DELETE); + for (int j = 1; j <= second.length(); ++j) { + int n2 = second.tokenAt(j - 1); + if (n1 == n2) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + B.set(i, j, EdOp.KEEP); + } else { + A[i % 2][j] = Math.min(A[(i - 1) % 2][j] + 1, + Math.min(A[i % 2][j - 1] + 1, + A[(i - 1) % 2][j - 1] + 1)); + if (A[i % 2][j] == A[(i - 1) % 2][j] + 1) { + B.set(i, j, EdOp.DELETE); + } else if (A[i % 2][j] == A[i % 2][j - 1] + 1) { + B.set(i, j, EdOp.INSERT); + } else { + B.set(i, j, EdOp.SUBSTITUTE); + } + } + } + } + + // extract sequence of edit operations + int i = B.width - 1; + int j = B.height - 1; + + ops = new ArrayList(); + + while (i > 0 || j > 0) { + EdOp e = B.get(i, j); + switch (e) { + case INSERT: + --j; + break; + case DELETE: + --i; + break; + default: + --i; + --j; + break; + } + add(e); + } + if (i != 0 || j != 0) { + throw new java.lang.IllegalArgumentException("Unvalid EditTable"); + } else { + Collections.reverse(ops); + } + } + + /** + * Linear-time approximation to the construction of the EditSequence for a + * pair of TokenArrays. The complexity gets reduced by splitting the arrays + * into smaller, overlapping, chunks. + * + * @param a1 the first TokenArray + * @param a2 the second TokenArray + * @param chunkLen the length of the chunks in which the arrays are split + * + */ + public EditSequence(TokenArray a1, TokenArray a2, int chunkLen) { + int len1 = a1.length(); + int len2 = a2.length(); + + if (chunkLen < 2) { + throw new IllegalArgumentException("chunkLen mut be greater than 1"); + } + + ops = new ArrayList(); + while (shift1() < len1 || shift2() < len2) { + int high1 = Math.min(shift1() + chunkLen, len1); + int high2 = Math.min(shift2() + chunkLen, len2); + TokenArray sub1 = a1.subArray(shift1(), high1); + TokenArray sub2 = a2.subArray(shift2(), high2); + EditSequence subseq = new EditSequence(sub1, sub2); + + EditSequence head = (high1 < len1 || high2 < len2) + ? subseq.head(subseq.size() / 2) + : subseq; + + append(head); + } + } + + /** + * Extract alignment statistics + * + * @param s1 the source string + * @param s2 the target string + * @return the statistics on the number of edit operations (per character + * and type of operation) + */ + public BiCounter stats(String s1, String s2) { + BiCounter stats = new BiCounter(); + int n1 = 0; + int n2 = 0; + for (EdOp op : ops) { + if (op != EdOp.INSERT) { + stats.inc(s1.charAt(n1), op); + ++n1; + } else { + stats.inc(s2.charAt(n2), EdOp.INSERT); + } + if (op != EdOp.DELETE) { + ++n2; + } + } + return stats; + } + + /** + * Extract alignment statistics + * + * @param s1 the source string + * @param s2 the target string + * @param w weights of basic edit operations + * @return the statistics on the number of edit operations (per character + * and type of operation) + */ + public BiCounter stats(String s1, String s2, EdOpWeight w) { + BiCounter stats = new BiCounter(); + int n1 = 0; + int n2 = 0; + for (EdOp op : ops) { + switch (op) { + case INSERT: + if (w.ins(s2.charAt(n2)) > 0) { + stats.inc(s2.charAt(n2), op); + } // costless insertion is equivalent to neglegible character + ++n2; + break; + case SUBSTITUTE: + if (w.sub(s1.charAt(n1), s2.charAt(n2)) > 0) { + stats.inc(s1.charAt(n1), op); + } else { // costless SUBSTITUTE is equivalent to KEEP + stats.inc(s1.charAt(n1), EdOp.KEEP); + } + ++n1; + ++n2; + break; + case DELETE: + if (w.del(s1.charAt(n1)) > 0) { + stats.inc(s1.charAt(n1), op); + } // costless deletion is equivalent to neglegible character + ++n1; + break; + case KEEP: + stats.inc(s1.charAt(n1), op); + ++n1; + ++n2; + break; + } + } + return stats; + } + + /** + * Compute cost of the transformation + * + * @param s1 the source string + * @param s2 the target string + * @param w the cost of basic edit operations + * @return the added cost of the edit operations (not identical to length + * because some operation may be free) + */ + public int cost(String s1, String s2, EdOpWeight w) { + int added = 0; + int n1 = 0; + int n2 = 0; + for (EdOp op : ops) { + switch (op) { + case INSERT: + added += w.ins(s2.charAt(n2)); + ++n2; + break; + case SUBSTITUTE: + added += w.sub(s1.charAt(n1), s2.charAt(n2)); + ++n1; + ++n2; + break; + case DELETE: + added += w.del(s1.charAt(n1)); + ++n1; + break; + case KEEP: + ++n1; + ++n2; + break; + } + } + return added; + } + + public static void main(String[] args) + throws Exception { + File gtfile = new File(args[0]); + File ocrfile = new File(args[1]); + String gts = new Text(gtfile).toString(); + String ocrs = new Text(ocrfile).toString(); + EdOpWeight w = new OcrOpWeight(); + EditSequence eds = new EditSequence(gts, ocrs, w, 2000); + System.out.println(eds); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditTable.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditTable.java new file mode 100644 index 00000000..1f95b943 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/EditTable.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +/** + * A compact structure storing the table of edit operations obtained during the + * computation of the edit distance between two sequences a1a2...am and + * b1b2...bn: each cell (i,j) in the table contains the last edit operation in + * the optimal sequence of editions transforming prefix a1a2...ai into prefix + * b1b2...bj. This table supports the retrieval of the full optimal edit + * sequence (equivalent to a shortest path problem). + * + * @author R.C.C. + */ +public class EditTable { + + int width; // table width = max i-value (exclusive) + int height; // table height = max j-value (exclusive) + byte[] bytes; // table content + + /** + * Create an EditTable with width rows and height columns + * + * @param width + * @param height + */ + public EditTable(int width, int height) { + if (Integer.MAX_VALUE / width / height < 4) { + throw new IllegalArgumentException("EditTable is too large"); + } else { + int len = (width / 4) * height + + (width % 4) * (height / 4) + + ((width % 4) * (height % 4)) + 1; // ceiling + this.width = width; + this.height = height; + bytes = new byte[len]; // two bits per operation + } + } + + /** + * Get a specific bit in a byte + * + * @param b + * a byte + * @param position + * the bit position + * @return the byte with that bit set to the specified value + */ + private static boolean getBit(byte b, int position) { + return ((b >> position) & 1) == 1; + } + + /** + * Return a new byte with one bit set to a specific value + * + * @param b + * a byte + * @param position + * the bit position + * @param value + * the value for that bit + * @return a new byte with that bit set to the specified value + */ + private static byte setBit(byte b, int position, boolean value) { + if (value) { + return b |= 1 << position; + } else { + return b |= 0 << position; + } + } + + /** + * Get the bit at that position in the byte array + * + * @param position + * a position in the array + * @return the bit at that position in the byte array + */ + private boolean getBit(int position) { + return getBit(bytes[position / 8], position % 8); + } + + /** + * Set the bit at that position in the byte array to the specified value + * + * @param position + * a position in the array + */ + private void setBit(int position, boolean value) { + bytes[position / 8] = setBit(bytes[position / 8], position % 8, value); + } + + /** + * + * @param i + * x-coordinate + * @param j + * y-coordinate + * @return the edit operation stored at cell (i,j) + * @throws IllegalArgumentException + */ + public EdOp get(int i, int j) { + int bytenum = (i / 4) * height + (i % 4) * (height / 4) + (j / 4) + + ((i % 4) * (height % 4) + (j % 4)) / 4; + int offset = (2 * ((i % 8) * (height % 8) + (j % 8))) % 8; + // int position = 2 * (i * height + j); + + try { + boolean low = getBit(bytes[bytenum], offset); + // boolean low = getBit(position); + boolean high = getBit(bytes[bytenum], offset + 1); + // boolean high = getBit(position + 1); + if (low) { + if (high) { + return EdOp.SUBSTITUTE; + } else { + return EdOp.DELETE; + } + } else { + if (high) { + return EdOp.INSERT; + } else { + return EdOp.KEEP; + } + } + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalArgumentException("Forbiden acces to " + + "cell (" + i + "," + j + + ") in EditTable of size (" + width + "," + height + ")"); + } + } + + /** + * Store an edit operation at cell (i, j) + * + * @param i + * x-coordinate + * @param j + * y-coordinate + * @param op + * the edit operation to be stored + */ + public void set(int i, int j, EdOp op) { + // int position = 2 * (i * height + j); + int bytenum = (i / 4) * height + (i % 4) * (height / 4) + (j / 4) + + ((i % 4) * (height % 4) + (j % 4)) / 4; + int offset = (2 * ((i % 8) * (height % 8) + (j % 8))) % 8; + + boolean low; + boolean high; + + switch (op) { + case SUBSTITUTE: + low = true; + high = true; + break; + case DELETE: + low = true; + high = false; + break; + case INSERT: + low = false; + high = true; + break; + case KEEP: + low = false; + high = false; + break; + default: + low = false; + high = false; + } + try { + bytes[bytenum] = setBit(bytes[bytenum], offset, low); + // setBit(position, low); + bytes[bytenum] = setBit(bytes[bytenum], offset + 1, high); + // setBit(position + 1, high); + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalArgumentException("Forbiden acces to " + + "cell (" + i + "," + j + + ") in EditTable of size (" + width + "," + height + ")"); + } + } + + /** + * + * @return a string representation of the EditTable + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + EdOp e = get(i, j); + switch (e) { + case KEEP: + builder.append('K'); + break; + case SUBSTITUTE: + builder.append('S'); + break; + case INSERT: + builder.append('I'); + break; + case DELETE: + builder.append('D'); + break; + } + } + builder.append("\n "); + } + + return builder.toString(); + } + + /** + * Build the sequence of edit operations in the path from a the cell at + * (row, column) to the cell at (0,0) + * + * @param row + * the starting row + * @param column + * the starting column + * @return the sequence of edit operations stored in this table which lead + * from the cell at (row, column) to the cell at (0,0) + */ + private EditSequence path(int row, int column) { + EditSequence operations = new EditSequence(Math.max(row, column)); + int i = row; + int j = column; + + while (i > 0 || j > 0) { + EdOp e = get(i, j); + switch (e) { + case INSERT: + --j; + break; + case DELETE: + --i; + break; + default: + --i; + --j; + break; + } + operations.add(e); + } + return (i == 0 && j == 0) ? operations : null; + } + + /** + * Build the sequence of edit operations in the path from a the cell at + * (width, height) to the cell at (0,0) + * + * @return the sequence of edit operations stored in this table which lead + * from the cell at (width - 1, height - 1) to the cell at (0,0) + * @depecated Use new EditSequence(EditTeble) instead + */ + public EditSequence path() { + return path(width - 1, height - 1); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/OcrOpWeight.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/OcrOpWeight.java new file mode 100644 index 00000000..10d529a6 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/OcrOpWeight.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.input.Parameters; + +/** + * Integer weights for basic edit operations. + * + * @author R.C.C. + */ +public class OcrOpWeight implements EdOpWeight { + + // boolean ignoreCase; + // boolean ignoreDiacritics; + boolean ignorePunctuation; + + /** + * + * @param ignoreCase true if case must be ignored + * @param ignoreDiacritics true if diacritics must be ignored + * @param ignorePunctuation true if punctuation must be ignored + */ + public OcrOpWeight(boolean ignorePunctuation) { + // this.ignoreCase = ignoreCase; + // this.ignoreDiacritics = ignoreDiacritics; + this.ignorePunctuation = ignorePunctuation; + } + + public OcrOpWeight(Parameters pars) { + this(pars.ignorePunctuation.getValue()); + } + + /** + * Default constructor creates weights which are case-sensitive, + * diacritics-aware and punctuation-aware. + */ + public OcrOpWeight() { + this(false); + } + + /** + * + * @param c1 the character found in text + * @param c2 the replacing character + * @return the cost of substituting character c1 with c2. Note: whitespace + * must not substitute character which are rather deleted; therefore, such + * cases return a value greater than 2 (standard insertion+deletion). + * Diacritics and case cannot be compared here due to efficiency reasons + * (too slow). + */ + @Override + public int sub(char c1, char c2) { + return (Character.isSpaceChar(c1) ^ Character.isSpaceChar(c2)) ? 4 : 1; + /* + if (Character.isSpaceChar(c1) ^ Character.isSpaceChar(c2)) { + return 4; // replacing whitespace with character is not recommended + } else if (ignoreCase) { + if (ignoreDiacritics) { + String s1 = StringNormalizer.removeDiacritics(String.valueOf(c1)); + String s2 = StringNormalizer.removeDiacritics(String.valueOf(c2)); + return (s1.toLowerCase().equals(s2.toLowerCase())) ? 0 : 1; + } else { + return (Character.toLowerCase(c1) == Character.toLowerCase(c2)) + ? 0 : 1; + } + + } else if (ignoreDiacritics) { // case matters + String s1 = StringNormalizer.removeDiacritics(String.valueOf(c1)); + String s2 = StringNormalizer.removeDiacritics(String.valueOf(c2)); + return (s1.equals(s2)) ? 0 : 1; + } else { + return c1 == c2 ? 0 : 1; + } + */ + } + + /** + * + * @param c a character + * @return the cost of inserting a character c + */ + @Override + public int ins(char c) { + if (ignorePunctuation) { + return (Character.isSpaceChar(c) || Character.isLetterOrDigit(c)) + ? 1 : 0; + } else { + return 1; + } + } + + /** + * + * @param c a character + * @return the cost of removing a character c + */ + @Override + public int del(char c) { + if (ignorePunctuation) { + return (Character.isSpaceChar(c) || Character.isLetterOrDigit(c)) + ? 1 : 0; + } else { + return 1; + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/StringEditDistance.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/StringEditDistance.java new file mode 100644 index 00000000..9cdca62c --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/StringEditDistance.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.math.BiCounter; +import eu.digitisation.output.ErrorMeasure; +import eu.digitisation.log.Messages; + +/** + * Provides basic implementations of some popular edit distance methods + * operating on strings (currently, Levenshtein and indel) + * + * @version 2011.03.10 + */ +public class StringEditDistance { + + /** + * @return 3-wise minimum. + */ + private static int min(int x, int y, int z) { + return Math.min(x, Math.min(y, z)); + } + + /** + * @param first the first string. + * @param second the second string. + * @return the indel distance between first and second. + */ + public static int indel(String first, String second) { + int i, j; + int[][] A = new int[2][second.length() + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length(); ++i) { + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + for (j = 1; j <= second.length(); ++j) { + if (first.charAt(i - 1) == second.charAt(j - 1)) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + } else { + A[i % 2][j] = Math.min(A[(i - 1) % 2][j] + 1, + A[i % 2][j - 1] + 1); + } + } + } + return A[first.length() % 2][second.length()]; + } + + /** + * @param first the first string. + * @param second the second string. + * @return the Levenshtein distance between first and second. + */ + public static int levenshtein(String first, String second) { + int i, j; + int[][] A; + + // intialize + A = new int[2][second.length() + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length(); ++i) { + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + for (j = 1; j <= second.length(); ++j) { + if (first.charAt(i - 1) == second.charAt(j - 1)) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + } else { + A[i % 2][j] = min(A[(i - 1) % 2][j] + 1, + A[i % 2][j - 1] + 1, + A[(i - 1) % 2][j - 1] + 1); + } + } + } + return A[first.length() % 2][second.length()]; + } + + /** + * @param first the first string. + * @param second the second string. + * @return the Damerau-Levenshtein distance between first and second. + */ + public static int DL(String first, String second) { + int i, j; + int[][] A; + + // intialize + A = new int[3][second.length() + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length(); ++i) { + A[i % 3][0] = A[(i - 1) % 3][0] + 1; + for (j = 1; j <= second.length(); ++j) { + if (first.charAt(i - 1) == second.charAt(j - 1)) { + A[i % 3][j] = A[(i - 1) % 3][j - 1]; + } else { + if (i > 1 && j > 1 + && first.charAt(i - 1) == second.charAt(j - 2) + && first.charAt(i - 2) == second.charAt(j - 1)) { + A[i % 3][j] = min(A[(i - 1) % 3][j] + 1, + A[i % 3][j - 1] + 1, + A[(i - 2) % 3][j - 2] + 1); + } else { + A[i % 3][j] = min(A[(i - 1) % 3][j] + 1, + A[i % 3][j - 1] + 1, + A[(i - 1) % 3][j - 1] + 1); + } + } + } + } + return A[first.length() % 3][second.length()]; + } + + /** + * + * @param first the first string. + * @param second the second string. + * @param type the type of distance to be computed + * @return the distance between first and second (defaults to Levenshtein) + */ + public static int distance(String first, String second, EditDistanceType type) { + switch (type) { + case INDEL: + return indel(first, second); + case LEVENSHTEIN: + return levenshtein(first, second); + case DAMERAU_LEVENSHTEIN: + return DL(first, second); + default: + return levenshtein(first, second); + } + } + + /** + * Computes the number of edit operations per character + * + * @param first the reference text + * @param second the fuzzy text + * @return a counter with the number of insertions, substitutions and + * deletions for every character + */ + public static BiCounter operations(String first, String second) { + int i, j; + int[][] A; + EditTable B; + BiCounter stats = new BiCounter(); + + // intialize + A = new int[2][second.length() + 1]; + B = new EditTable(first.length() + 1, second.length() + 1); + // Compute first row + A[0][0] = 0; + B.set(0, 0, EdOp.KEEP); + for (j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + B.set(0, j, EdOp.INSERT); + } + + // Compute other rows + for (i = 1; i <= first.length(); ++i) { + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + B.set(i, 0, EdOp.DELETE); + for (j = 1; j <= second.length(); ++j) { + if (first.charAt(i - 1) == second.charAt(j - 1)) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + B.set(i, j, EdOp.KEEP); + } else { + A[i % 2][j] = min(A[(i - 1) % 2][j] + 1, + A[i % 2][j - 1] + 1, + A[(i - 1) % 2][j - 1] + 1); + if (A[i % 2][j] == A[(i - 1) % 2][j] + 1) { + B.set(i, j, EdOp.DELETE); + } else if (A[i % 2][j] == A[i % 2][j - 1] + 1) { + B.set(i, j, EdOp.INSERT); + } else { + B.set(i, j, EdOp.SUBSTITUTE); + } + } + } + } + + i = first.length(); + j = second.length(); + while (i > 0 && j > 0) { + switch (B.get(i, j)) { + case KEEP: + stats.inc(first.charAt(i - 1), EdOp.KEEP); + --i; + --j; + break; + case DELETE: + stats.inc(first.charAt(i - 1), EdOp.DELETE); + --i; + break; + case INSERT: + stats.inc(second.charAt(j - 1), EdOp.INSERT); + --j; + break; + case SUBSTITUTE: + stats.inc(first.charAt(i - 1), EdOp.SUBSTITUTE); + + --i; + --j; + break; + } + } + while (i > 0) { + stats.inc(first.charAt(i - 1), EdOp.DELETE); + --i; + } + while (j > 0) { + stats.inc(second.charAt(j - 1), EdOp.INSERT); + --j; + + } + + return stats; + } + + /** + * Aligns two strings (one to one alignments with substitutions). + * + * @param first the first string. + * @param second the second string. + * @return the mapping between positions. + * @deprecated use Aligner class + */ + public static int[] alignment(String first, String second) { + int i, j; + int[][] A; + + // intialize + A = new int[first.length() + 1][second.length() + 1]; + + // Compute first row + A[0][0] = 0; + for (j = 1; j <= second.length(); ++j) { + A[0][j] = A[0][j - 1] + 1; + } + + // Compute other rows + for (i = 1; i <= first.length(); ++i) { + A[i][0] = A[i - 1][0] + 1; + for (j = 1; j <= second.length(); ++j) { + if (first.charAt(i - 1) == second.charAt(j - 1)) { + A[i][j] = A[i - 1][j - 1]; + } else { + A[i][j] = min(A[i - 1][j] + 1, A[i][j - 1] + 1, + A[i - 1][j - 1] + 1); + } + } + } + + int[] alignments = new int[first.length()]; + java.util.Arrays.fill(alignments, -1); + + i = first.length(); + j = second.length(); + while (i > 0 && j > 0) { + if (first.charAt(i - 1) == second.charAt(j - 1) + || A[i][j] == A[i - 1][j - 1] + 1) { + alignments[--i] = --j; + } else if (A[i][j] == A[i - 1][j] + 1) { + --i; + } else if (A[i][j] == A[i][j - 1] + 1) { + --j; + } else { // remove after debugging + Messages.info(ErrorMeasure.class.getName() + ": Wrong code"); + } + } + + return alignments; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/distance/WordCompare.java b/ocrevalUAtion/src/main/java/eu/digitisation/distance/WordCompare.java new file mode 100644 index 00000000..6431d2f3 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/distance/WordCompare.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.document.TokenArray; +import eu.digitisation.math.MinimalPerfectHash; + +/** + * Word alignments between 2 texts (output in text format) + * + * @author R.C.C + */ +public class WordCompare { + + /** + * @return 3-wise minimum. + */ + private static int min(int x, int y, int z) { + return Math.min(x, Math.min(y, z)); + } + + /** + * Compute the table of basic edit operations needed to transform first into + * second + * + * @param first source string + * @param second target string + * @return the table of minimal basic edit operations needed to transform + * first into second + */ + private static EditTable align(TokenArray a1, TokenArray a2) { + + int l1 = a1.length(); // length of first + int l2 = a2.length(); // length of second + int[][] A; // distance table + EditTable B; // edit operations + + // intialize + A = new int[2][l2 + 1]; + B = new EditTable(l1 + 1, l2 + 1); + // Compute first row + A[0][0] = 0; + B.set(0, 0, EdOp.KEEP); + for (int j = 1; j <= l2; ++j) { + A[0][j] = A[0][j - 1] + 1; + B.set(0, j, EdOp.INSERT); + } + + // Compute other rows + for (int i = 1; i <= l1; ++i) { + int n1 = a1.tokenAt(l1 - i); + A[i % 2][0] = A[(i - 1) % 2][0] + 1; + B.set(i, 0, EdOp.DELETE); + for (int j = 1; j <= l2; ++j) { + int n2 = a2.tokenAt(l2 - j); + + if (n1 == n2) { + A[i % 2][j] = A[(i - 1) % 2][j - 1]; + B.set(i, j, EdOp.KEEP); + } else { + A[i % 2][j] = min(A[(i - 1) % 2][j] + 1, + A[i % 2][j - 1] + 1, + A[(i - 1) % 2][j - 1] + 1); + if (A[i % 2][j] == A[(i - 1) % 2][j] + 1) { + B.set(i, j, EdOp.DELETE); + } else if (A[i % 2][j] == A[i % 2][j - 1] + 1) { + B.set(i, j, EdOp.INSERT); + } else { + B.set(i, j, EdOp.SUBSTITUTE); + } + } + } + } + return B; + } + + /** + * Show word-level differences based on a Levenshtein distance + * + * @param first the first text + * @param second the second text + * @return a report on the differences between files + */ + public static String wdiff(String first, String second) { + MinimalPerfectHash mph = new MinimalPerfectHash(false); // case unsensitive + TokenArray a1 = new TokenArray(mph, first); + TokenArray a2 = new TokenArray(mph, second); + EditTable B = align(a1, a2); + StringBuilder builder = new StringBuilder(); + + int l1 = a1.length(); + int l2 = a2.length(); + int i = l1; + int j = l2; + + while (i > 0 && j > 0) { + switch (B.get(i, j)) { + case KEEP: + builder.append(a1.wordAt(l1 - i)).append(" = ") + .append(a2.wordAt(l2 - j)).append('\n'); + --i; + --j; + break; + case DELETE: + builder.append(a1.wordAt(l1 - i)).append(" # []\n"); + --i; + break; + case INSERT: + builder.append("[] # ") + .append(a2.wordAt(l2 - j)).append('\n'); + --j; + break; + case SUBSTITUTE: + builder.append(a1.wordAt(l1 - i)) + .append(" # ").append(a2.wordAt(l2 - j)).append('\n'); + --i; + --j; + break; + } + + } + if (i > 0) { + builder.append(a1.wordAt(l1 - i)).append(" # []\n"); + --i; + } + if (j > 0) { + builder.append("[] # ") + .append(a2.wordAt(l2 - j)).append('\n'); + --j; + } + + return builder.toString(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/document/TermFrequencyVector.java b/ocrevalUAtion/src/main/java/eu/digitisation/document/TermFrequencyVector.java new file mode 100644 index 00000000..e2973edc --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/document/TermFrequencyVector.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.document; + +import eu.digitisation.math.Counter; +import eu.digitisation.math.MinimalPerfectHash; + +/** + * A term frequency vector stores counts for every word in text. + * + * @author R.C.C. + */ +public class TermFrequencyVector { + + static MinimalPerfectHash mph = new MinimalPerfectHash(); + Counter tf; + + /** + * Create a TermFrequencyVector from a String + * + * @param s + */ + public TermFrequencyVector(String s) { + TokenArray array = new TokenArray(mph, s); + + tf = new Counter(); + for (Integer n : array) { + tf.inc(n); + } + } + + /** + * Compute the distance between two bag of words (order independent + * distance) + * + * @param other another bag of words + * @return the number of differences between this and the other bag of words + */ + public int distance(TermFrequencyVector other) { + int dplus = 0; // excess + int dminus = 0; // fault + for (Integer word : this.tf.keySet()) { + int delta = this.tf.value(word) - other.tf.value(word); + if (delta > 0) { + dplus += delta; + } else { + dminus -= delta; + } + } + for (Integer word : other.tf.keySet()) { + if (!this.tf.containsKey(word)) { + int delta = this.tf.value(word) - other.tf.value(word); + if (delta > 0) { + dplus += delta; + } else { + dminus -= delta; + } + } + } + + return Math.max(dplus, dminus); + } + + /** + * The total number of words + * + * @return the total number of words + */ + public int total() { + return tf.total(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (Integer code : tf.keyList(Counter.Order.ASCENDING)) { + String s = mph.decode(code); + builder.append(s).append('[').append(tf.get(code)).append("] "); + } + return builder.toString().trim(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/document/TokenArray.java b/ocrevalUAtion/src/main/java/eu/digitisation/document/TokenArray.java new file mode 100644 index 00000000..a13c3cce --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/document/TokenArray.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.document; + +import eu.digitisation.distance.ArrayEditDistance; +import eu.digitisation.distance.EditDistanceType; +import eu.digitisation.math.MinimalPerfectHash; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * A TokenArray is a tokenized string: every word is internally stored as an + * integer. The mapping between words and integer codes is shared by all + * TokenArrays created with the same MinimalPerfectHash. + * + * @version 2013.12.10 + */ +public class TokenArray implements Iterable { + + MinimalPerfectHash mph; // the creator mph + Integer[] tokens; // the content + + /** + * Default constructor + * + * @param interpretation the dictionary of codes + * @param tokens the integer representation + */ + TokenArray(MinimalPerfectHash mph, Integer[] tokens) { + this.mph = mph; + this.tokens = tokens; + } + + /** + * Default constructor + * + * @param interpretation the dictionary of codes + * @param tokens the integer representation + * + */ + TokenArray(MinimalPerfectHash mph, List tokens) { + this.mph = mph; + this.tokens = tokens.toArray(new Integer[tokens.size()]); + + } + + /** + * + * @param mph the hashing scheme + * @param s the input string to be tokenized + */ + public TokenArray(MinimalPerfectHash mph, String s) { + this(mph, mph.hashCodes(s)); + } + + /** + * The length of the token array + * + * @return the length of the token array + */ + public int length() { + return tokens.length; + } + + /** + * + * @return the internal representation as an array of integer codes + */ + public Integer[] tokens() { + return tokens; + } + + /** + * Value of token at a given position + * + * @param pos a position in the array + * @return the value associated to this position + */ + public int tokenAt(int pos) { + return tokens[pos]; + } + + /** + * + * @param pos a position in the array + * @return the word or string at this position in the array + */ + public String wordAt(int pos) { + Integer token = tokens[pos]; + return mph.decode(token); + } + + /** + * Create a TokenArray with a range of another TokenArray + * @param fromIndex low endpoint (inclusive) of the subArray + * @param toIndex high endpoint (exclusive) of the subArray + * @return + */ + public TokenArray subArray(int fromIndex, int toIndex) { + List sublist = Arrays.asList(tokens).subList(fromIndex, toIndex); + return new TokenArray(mph, sublist); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + for (Integer token : tokens) { + builder.append(mph.decode(token)).append(" "); + } + + return builder.toString().trim(); + } + + /** + * Distance between TokenArrays + * + * @param other another TokenArray + * @param type the distance type + * @return the distance between this and the other TokenArray + */ + public int distance(TokenArray other, EditDistanceType type) { + return ArrayEditDistance.distance(this.tokens, other.tokens, type); + } + + /** + * Return the TokenArray as array + * + * @return the array of tokens + */ + public String array() { + return java.util.Arrays.toString(tokens); + } + + @Override + public Iterator iterator() { + return Arrays.asList(tokens).iterator(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/image/Bimage.java b/ocrevalUAtion/src/main/java/eu/digitisation/image/Bimage.java new file mode 100644 index 00000000..b22230ee --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/image/Bimage.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.image; + +import eu.digitisation.math.Counter; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Polygon; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.io.IOException; +import java.util.List; +import javax.media.jai.JAI; + +/** + * Extends BufferedImage with some useful operations + * + * @author R.C.C. + */ +public class Bimage extends BufferedImage { + + static int defaultImageType = BufferedImage.TYPE_INT_RGB; + + /** + * Basic constructor + * + * @param width + * @param height + * @param imageType + */ + public Bimage(int width, int height, int imageType) { + super(width, height, imageType); + } + + /** + * Basic constructor + * + * @param width + * @param height + */ + public Bimage(int width, int height) { + super(width, height, defaultImageType); + } + + /** + * Create a BufferedImage from another BufferedImage. Type set to default in + * case of TYPE_CUSTOM (not handled by BufferedImage) . + * + * @param image the source image + */ + public Bimage(BufferedImage image) { + super(image.getWidth(null), image.getHeight(null), + image.getType() == BufferedImage.TYPE_CUSTOM + ? defaultImageType + : image.getType()); + Graphics2D g = createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + } + + /** + * Create a BufferedImage of the given type from another BufferedImage. + * + * @param image the source image + * @param type the type of BufferedImage + */ + public Bimage(BufferedImage image, int type) { + super(image.getWidth(null), image.getHeight(null), type); + Graphics2D g = createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + } + + /** + * Create image from file. + * + * @param file the file storing the image + * @throws IOException + * @throws NullPointerException if the file format is unsupported + */ + public Bimage(java.io.File file) throws IOException { + // this(javax.imageio.ImageIO.read(file)); + this(JAI.create("FileLoad", + file.getCanonicalPath()).getAsBufferedImage()); + } + + /** + * Create a scaled image + * + * @param img the source image + * @param scale the scale factor + */ + public Bimage(BufferedImage img, double scale) { + super((int) (scale * img.getWidth()), + (int) (scale * img.getHeight()), + img.getType()); + int hints = java.awt.Image.SCALE_SMOOTH; //scaling algorithm + Image scaled = img.getScaledInstance(this.getWidth(), + this.getHeight(), + hints); + Graphics2D g = createGraphics(); + g.drawImage(scaled, 0, 0, null); + g.dispose(); + } + + /** + * Finds the background (statistical mode of the rgb value for pixels in the + * image) + * + * @return the mode of the color for pixels in this image + */ + private Color background() { + Counter colors = new Counter(); + + for (int x = 0; x < getWidth(); ++x) { + for (int y = 0; y < getHeight(); ++y) { + int rgb = getRGB(x, y); + colors.inc(rgb); + } + } + + Integer mu = colors.maxValue(); + for (Integer n : colors.keySet()) { + if (colors.get(n).equals(mu)) { + return new Color(n); + } + } + return null; + } + + /** + * Create a scaled image + * + * @param scale the scale factor + * @return a scaled image + */ + public Bimage scale(double scale) { + int w = (int) (scale * getWidth()); + int h = (int) (scale * getHeight()); + Bimage scaled = new Bimage(w, h, getType()); + //int hints = java.awt.Image.SCALE_SMOOTH; //scaling algorithm + //Image img = getScaledInstance(w, h, hints); + Graphics2D g = scaled.createGraphics(); + AffineTransform at = new AffineTransform(); + at.scale(scale, scale); + g.drawImage(this, at, null); + g.dispose(); + return scaled; + } + + /** + * Create a rotated image + * + * @param alpha the rotation angle (anticlockwise) + * @return the rotated image + */ + public Bimage rotate(double alpha) { + double cos = Math.cos(alpha); + double sin = Math.abs(Math.sin(alpha)); + int w = (int) Math.floor(getWidth() * cos + getHeight() * sin); + int h = (int) Math.floor(getHeight() * cos + getWidth() * sin); + Bimage rotated = new Bimage(w, h, getType()); + Graphics2D g = (Graphics2D) rotated.getGraphics(); + g.setBackground(background()); + g.clearRect(0, 0, w, h); + if (alpha < 0) { + g.translate(getHeight() * sin, 0); + } else { + g.translate(0, getWidth() * sin); + } + g.rotate(-alpha); + g.drawImage(this, 0, 0, null); + g.dispose(); + return rotated; + + } + + /** + * Create a new image from two layers (with the type of first) + * + * @param first the first source image + * @param second the second source image + */ + public Bimage(BufferedImage first, BufferedImage second) { + super(Math.max(first.getWidth(), second.getWidth()), + Math.max(first.getHeight(), second.getHeight()), + first.getType()); + BufferedImage combined = new BufferedImage(this.getWidth(), + this.getHeight(), + this.getType()); + Graphics2D g = combined.createGraphics(); + g.drawImage(first, 0, 0, null); + g.drawImage(second, 0, 0, null); + g.dispose(); + } + + /** + * Transform image to gray-scale + * + * @return this image as gray-scale image + */ + public Bimage toGrayScale() { + ColorSpace space = ColorSpace.getInstance(ColorSpace.CS_GRAY); + ColorConvertOp operation = new ColorConvertOp(space, null); + return new Bimage(operation.filter(this, null)); + } + + /** + * Transform image to RGB + * + * @return this image as RGB image + */ + public Bimage toRGB() { + Bimage bim = new Bimage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = bim.createGraphics(); + g.drawImage(this, 0, 0, null); + g.dispose(); + return bim; + } + + /** + * Clear the image to white + */ + public void clear() { + Graphics2D g = createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, getWidth(), getHeight()); + g.dispose(); + } + + /** + * Add a polygonal frontier to the image + * + * @param p a polygon + * @param color the color of the polygon + * @param stroke the line width in pixels + */ + public void add(Polygon p, Color color, float stroke) { + Graphics2D g = createGraphics(); + g.setColor(color); + g.setStroke(new BasicStroke(stroke)); + g.drawPolygon(p); + g.dispose(); + } + + /** + * Add a dashed polygonal frontier to the image + * + * @param p a polygon + * @param color the color of the polygon + * @param stroke the line width in pixels + * @param pattern the dash pattern, for example, {4f,2f} draws dashes with + * length 4-units and separated 2 units + */ + public void add(Polygon p, Color color, float stroke, float[] pattern) { + Graphics2D g = createGraphics(); + BasicStroke bs = new BasicStroke(stroke, BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, 1, pattern, 0.0f); + g.setColor(color); + g.setStroke(bs); + g.drawPolygon(p); + g.dispose(); + } + + /** + * Add polygonal frontiers to the image + * + * @param polygons list of polygonal regions + * @param color he color of the polygons + * @param stroke the line width in pixels + */ + public void add(List polygons, Color color, float stroke) { + for (Polygon p : polygons) { + add(p, color, stroke); + } + } + + /** + * Add polygonal frontiers to the image + * + * @param polygons an array of polygonal regions + * @param color he color of the polygons + * @param stroke the line width in pixels + * @param pattern the dash pattern, for example, {4f,2f} draws dashes + * 4-pixels long separated by 2 pixels + */ + public void add(Polygon[] polygons, Color color, float stroke, float[] pattern) { + for (Polygon p : polygons) { + add(p, color, stroke, pattern); + } + } + + /** + * Add polygonal frontiers to the image + * + * @param polygons an array of polygonal regions + * @param color he color of the polygons + * @param stroke the line width in pixels + * @param pattern the dash pattern, for example, {4f,2f} draws dashes + * 4-pixels long separated by 2 pixels + */ + public void add(List polygons, Color color, float stroke, float[] pattern) { + for (Polygon p : polygons) { + add(p, color, stroke, pattern); + } + } + + /** + * Write the image to a file + * + * @param file the output file + * @throws java.io.IOException + */ + public void write(java.io.File file) + throws IOException { + Format format = Format.valueOf(file); + JAI.create("filestore", this, + file.getCanonicalPath(), format.toString()); + //javax.imageio.ImageIO.write(this, format, file); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/image/Display.java b/ocrevalUAtion/src/main/java/eu/digitisation/image/Display.java new file mode 100644 index 00000000..865d072e --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/image/Display.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.image; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import javax.swing.JComponent; +import javax.swing.JFrame; + +/** + * Simple class to display an image on screen + * + * @author R.C.C. + */ +class ImageComponent extends JComponent { + private static final long serialVersionUID = 1L; + int x; + int y; + int width; + int height; + BufferedImage img = null; + + ImageComponent(BufferedImage img) { + this.img = img; + x = 0; + y = 0; + width = img.getWidth(); + height = img.getHeight(); + } + + ImageComponent(BufferedImage img, int x, int y, int width, int height) { + this.img = img; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + + @Override + public void paint(Graphics g) { + g.drawImage(img, x, y, width, height, this); + // g.finalize(); + } +} + +/** + * Plot an image on screen + * @author R.C.C: + */ +public class Display { + + JFrame window = null; + + public static void draw(BufferedImage img) { + JFrame window = new JFrame(); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.setBounds(0, 0, img.getWidth(), img.getHeight()); + window.getContentPane().add(new ImageComponent(img)); + window.setVisible(true); + } + + public static void draw(BufferedImage img, int width, int height) { + JFrame window = new JFrame(); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.setBounds(0, 0, width, height); + window.getContentPane().add(new ImageComponent(img, 0, 0,width, height)); + window.setVisible(true); + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/image/Format.java b/ocrevalUAtion/src/main/java/eu/digitisation/image/Format.java new file mode 100644 index 00000000..d65e3d33 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/image/Format.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.image; + +import java.io.IOException; +import java.util.Locale; + +/** + * Image input formats supported + * + * @author R.C.C + */ +@SuppressWarnings("javadoc") +public enum Format { + + BMP, FlashPix, GIF, JPEG, PNG, PNM, TIFF, WBMP; + + /** + * + * @param file the container file + * @return the Format associated with this file name extension + * @throws IOException + */ + public static Format valueOf(java.io.File file) throws IOException { + String name = file.getName(); + String ext = name + .substring(name.lastIndexOf('.') + 1) + .toLowerCase(Locale.ENGLISH); + if (ext.equals("bpm")) { + return BMP; + } else if (ext.equals("fpx")) { + return FlashPix; + } else if (ext.equals("gif")) { + return GIF; + } else if (ext.equals("jpg") || ext.equals("jpeg")) { + return JPEG; + } else if (ext.equals("png")) { + return PNG; + } else if (ext.equals("pnm")) { + return PNM; + } else if (ext.equals("tif") || ext.equals("tiff")) { + return TIFF; + } else if (ext.equals("wbmp")) { + return WBMP; + } else { + throw new IOException("Unsupported image format " + ext); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Batch.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Batch.java new file mode 100644 index 00000000..bdea0e41 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Batch.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.io.File; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.nio.file.Path; +import java.util.ArrayList; + +import eu.digitisation.math.Pair; + +/** + * A batch of file pairs to be processed. Files must be in two different folders + * and named unambiguously (a unique one-to-one mapping must be straightforward + * from file names). Alternatively a Batch consisting of a single file pair can + * be also created. + * + * @author R.C.C. + */ +public class Batch { + + int size; + File[] files1; + File[] files2; + + /** + * Create a a batch of file pairs + * + * @param dir1 + * the first directory of files + * @param dir2 + * the second directory of files + * @throws InvalidObjectException + */ + public Batch(File dir1, File dir2) throws InvalidObjectException { + if (dir1.isDirectory()) { + files1 = dir1.listFiles(); + java.util.Arrays.sort(files1); + } else { + files1 = new File[1]; + files1[0] = dir1; + } + if (dir2.isDirectory()) { + files2 = dir2.listFiles(); + java.util.Arrays.sort(files2); + } else { + files2 = new File[1]; + files2[0] = dir2; + } + if (files1.length != files2.length) { + throw new java.io.InvalidObjectException(dir1.getName() + + " and " + dir2.getName() + + " contain a different number of files"); + } else { + size = files1.length; + } + if (!consistent()) { + throw new java.io.InvalidObjectException(dir1.getName() + + " and " + dir2.getName() + + " contain files with inconsistent names"); + } + } + + /** + * + * @param transcriptions + * @param ocrDir + * @throws IOException + * + * @author Paul Vorbach + */ + public Batch(Iterable transcriptions, Path ocrDir) throws IOException { + final ArrayList fs1 = new ArrayList(); + final ArrayList fs2 = new ArrayList(); + + for (final Path transcription : transcriptions) { + fs1.add(transcription.toFile()); + fs2.add(ocrDir.resolve(transcription.getFileName()).toFile()); + } + + size = fs1.size(); + + files1 = new File[size]; + files2 = new File[size]; + + fs1.toArray(files1); + fs2.toArray(files2); + } + + private boolean consistent() { + if (size > 1) { + int low1 = lcp(files1).length(); + int low2 = lcp(files2).length(); + int high1 = lcs(files1).length(); + int high2 = lcs(files2).length(); + for (int n = 0; n < size; ++n) { + String name1 = files1[n].getName(); + String name2 = files2[n].getName(); + String id1 = name1.substring(low1, name1.length() - high1); + String id2 = name2.substring(low2, name2.length() - high2); + if (!id1.equals(id2)) { + return false; + } + } + } + return true; + } + + /** + * + * @return he number of file to be processed + */ + public int size() { + return size; + } + + /** + * + * @param n + * the file number + * @return the n-th File pair + */ + public Pair pair(int n) { + return new Pair(files1[n], files2[n]); + } + + /** + * Common prefix + * + * @param s1 + * a string + * @param s2 + * another string + * @return the common prefix of s1 and s2 + */ + static String prefix(String s1, String s2) { + int limit = Math.min(s1.length(), s2.length()); + int len = 0; + + while (len < limit && s1.charAt(len) == s2.charAt(len)) { + ++len; + } + return s1.substring(0, len); + } + + /** + * Longest common prefix + * + * @param files + * an array of files + * @return the longest common prefix to all filenames + */ + private String lcp(File[] files) { + if (files.length > 0) { + String result = files[0].getName(); + for (int n = 1; n < files.length; ++n) { + result = prefix(result, files[n].getName()); + } + return result; + } else { + return null; + } + } + + /** + * Common suffix + * + * @param s1 + * one word + * @param s2 + * another word + * @return the common suffix to s1 and s2 + */ + static String suffix(String s1, String s2) { + int limit = Math.min(s1.length(), s2.length()); + int len = 0; + + while (len < limit + && s1.charAt(s1.length() - len - 1) == s2.charAt(s2.length() + - len - 1)) { + ++len; + } + return s1.substring(s1.length() - len); + } + + /** + * Longest common suffix + * + * @param files + * an array of files + * @return the longest common suffix to all files + */ + public static String lcs(File[] files) { + if (files.length > 0) { + String result = files[0].getName(); + for (int n = 1; n < files.length; ++n) { + result = suffix(result, files[n].getName()); + } + return result; + } else { + return null; + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/BooleanSelector.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/BooleanSelector.java new file mode 100644 index 00000000..85a2ff7a --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/BooleanSelector.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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 param) 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; + +/** + * + * @author R.C.C + */ +public class BooleanSelector extends ParameterSelector { + + private static final long serialVersionUID = 1L; + + JCheckBox box; + + public BooleanSelector(Parameter op, Color forecolor, Color bgcolor) { + super(op, forecolor, bgcolor); + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + setPreferredSize(new Dimension(100, 30)); + setBackground(bgcolor); + + box = new JCheckBox(op.name); + box.setFont(new Font("Verdana", Font.BOLD, 12)); + box.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + param.value = box.isSelected(); + } + }); + add(box); + if (op.help != null && op.help.length() > 0) { + add(new Help(op.help, forecolor, bgcolor)); + } + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/ExtensionFilter.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/ExtensionFilter.java new file mode 100644 index 00000000..55f78b7d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/ExtensionFilter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.Locale; + +/** + * + * @author R.C.C. + */ +public class ExtensionFilter implements FilenameFilter { + + private final String ext; + + /** + * Create filter for files with the given extension + * @param ext the extension required + */ + public ExtensionFilter(String ext) { + this.ext = ext.toLowerCase(Locale.ENGLISH); + } + + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase(Locale.ENGLISH).endsWith(ext); + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/FileSelector.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/FileSelector.java new file mode 100644 index 00000000..cdcb8851 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/FileSelector.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JTextPane; + +/** + * + * @author R.C.C + */ +public class FileSelector extends ParameterSelector { + + static final long serialVersionUID = 1L; + static File dir; // directory opened by default + JTextPane area; // The area to display the filename + JButton choose; // Optional file chooser + + public FileSelector(Parameter op, Color forecolor, Color bgcolor) { + super(op, forecolor, bgcolor); + + setPreferredSize(new Dimension(600, 70)); + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + setBorder(BorderFactory.createLineBorder(forecolor, 2)); + + // Drop area + area = new JTextPane(); + area.setFont(new Font("Verdana", Font.PLAIN, 12)); + area.setText("Drop here your " + param.name); + area.setForeground(forecolor); + area.setBackground(bgcolor); + enableDragAndDrop(area); + + // File chooser + choose = new JButton("Or select the file"); + choose.setFont(new Font("Verdana", Font.PLAIN, 10)); + choose.setForeground(forecolor); + choose.setBackground(bgcolor); + choose.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + File file = choose("input_file"); + if (file != null) { + param.setValue(file); + area.setText(file.getName()); + dir = file.getParentFile(); + shade(Color.decode("#B5CC9E")); // green + } + } + }); + + // Put everything togetehr + add(area); + add(choose, BorderLayout.EAST); + } + + /** + * Set the background of panel (excluding choose JButton) + * + * @param color the background color + */ + public void shade(Color color) { + setBackground(color); + area.setBackground(color); + area.setForeground(Color.DARK_GRAY); + } + + /** + * Highlight if not ready + */ + public void checkout() { + if (!ready()) { + shade(Color.decode("#fffacd")); // yellow + } + } + + /** + * Change descriptive text + * + * @param text the text to be displayed + */ + public void setText(String text) { + area.setText(text); + } + + /** + * + * @return the selected input file + */ + public File getFile() { + return param.getValue(); + } + + /** + * + * @return true if a file has been selected and the file exists + */ + public boolean ready() { + File file = getFile(); + return file != null && file.exists(); + } + + /** + * The drag&drop function + * @param area + */ + private void enableDragAndDrop(final JTextPane area) { + DropTarget target; + target = new DropTarget(area, new DropTargetListener() { + + @Override + public void dragEnter(DropTargetDragEvent e) { + } + + @Override + public void dragExit(DropTargetEvent e) { + } + + @Override + public void dragOver(DropTargetDragEvent e) { + } + + @Override + public void dropActionChanged(DropTargetDragEvent e) { + } + + @Override + @SuppressWarnings("unchecked") + public void drop(DropTargetDropEvent e) { + try { + // Accept the drop first! + e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); + if (e.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + java.util.List list; + list = (java.util.List) e.getTransferable() + .getTransferData(DataFlavor.javaFileListFlavor); + param.setValue(list.get(0)); + } else if (e.isDataFlavorSupported(DataFlavor.stringFlavor)) { + String name = (String) e.getTransferable() + .getTransferData(DataFlavor.stringFlavor); + param.setValue(new File(new URI(name.trim()))); + } + area.setText(getFile().getName()); + dir = getFile().getParentFile(); + shade(Color.decode("#B5CC9E")); + } catch (URISyntaxException ex) { + Messages.info(FileSelector.class.getName() + ": " + ex); + } catch (IOException ex) { + Messages.info(FileSelector.class.getName() + ": " + ex); + } catch (UnsupportedFlavorException ex) { + Messages.info(FileSelector.class.getName() + ": " + ex); + } + } + }); + } + + /** + * Select file with a file selector (menu) + * + * @param defaultName the default name for the chosen file + * @return + */ + private File choose(String defaultName) { + JFileChooser chooser = new JFileChooser(); + + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + chooser.setDialogTitle("Select input file"); + chooser.setCurrentDirectory(dir); + chooser.setSelectedFile(new File(defaultName)); + int returnVal = chooser.showOpenDialog(FileSelector.this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + return chooser.getSelectedFile(); + } else { + return null; + } + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/FileType.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/FileType.java new file mode 100644 index 00000000..9397bb5f --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/FileType.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.xml.DocumentParser; +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.Properties; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Supported input file types + * + * @author R.C.C. + */ +@SuppressWarnings("javadoc") +public enum FileType { + + TEXT, PAGE, FR10, HOCR, ALTO, UNKNOWN; + String tag; + String schemaLocation; // schema URL + + static { + reload(); + } + + public static void reload() { + Properties props = Settings.properties(); + + TEXT.tag = null; // no tag for this type + TEXT.schemaLocation = null; // no schema associated to this type + + PAGE.tag = "PcGts"; + PAGE.schemaLocation = getSchemaLocation(props, "PAGE"); + + FR10.tag = "document"; + FR10.schemaLocation = getSchemaLocation(props, "FR10"); + + ALTO.tag = "alto"; + ALTO.schemaLocation = getSchemaLocation(props, "ALTO"); + + HOCR.tag = "html"; + HOCR.schemaLocation = null; // no schema for this type + } + + /** + * Load the schemaLocation from properties + * + * @param props properties + * @param suffix the schemaLocation suffix (e.g., ALTO, FR10) + * @return the property value + */ + public static String getSchemaLocation(Properties props, String suffix) { + String location = props.getProperty("schemaLocation." + suffix); + return (location == null) ? " " : StringNormalizer.reduceWS(location); + } + + /** + * + * @param locations1 string of URL schema locations separated by spaces + * @param locations2 string of URL schema locations separated by spaces + * @return True if at least one URL is in both locations + */ + private static boolean sameLocation(String locations1, String locations2) { + String[] urls = locations2.split("\\p{Space}+"); + + for (String url : urls) { + if (locations1.contains(url)) { + return true; + } + } + return false; + } + + /** + * + * @param file a file + * @return the FileType of file + */ + public static FileType valueOf(File file) throws SchemaLocationException { + String name = file.getName().toLowerCase(Locale.ENGLISH); + + if (name.endsWith(".txt")) { + return TEXT; + } else if (name.endsWith(".xml")) { + Document doc = DocumentParser.parse(file); + Element root = doc.getDocumentElement(); + String doctype = root.getTagName(); + String location; + + if (root.hasAttribute("xsi:schemaLocation")) { + location = StringNormalizer + .reduceWS(root.getAttribute("xsi:schemaLocation")); + } else if (root.hasAttribute("xsi:noNamespaceSchemaLocation")) { + location = StringNormalizer + .reduceWS(root.getAttribute("xsi:noNamespaceSchemaLocation")); + } else { + location = null; + } + + if (doctype.equals(PAGE.tag)) { + if (sameLocation(location, PAGE.schemaLocation)) { + return PAGE; + } else if (!location.isEmpty()) { + throw new SchemaLocationException(PAGE, location); + } + } else if (doctype.equals(FR10.tag)) { + if (sameLocation(location, FR10.schemaLocation)) { + return FR10; + } else if (!location.isEmpty()) { + throw new SchemaLocationException(FR10, location); + } + } else if (doctype.equals(ALTO.tag)) { + Messages.info(ALTO.schemaLocation); + if (sameLocation(location, ALTO.schemaLocation)) { + return ALTO; + } else if (!location.isEmpty()) { + throw new SchemaLocationException(ALTO, location); + } + } + } else if (name.endsWith(".html")) { + try { + org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parse(file, null); + if (!doc.head().select("meta[name=ocr-system").isEmpty()) { + return HOCR; + } + } catch (IOException ex) { + Messages.info(FileType.class + .getName() + ": " + ex); + } + } + return UNKNOWN; + } + + public static void main(String[] args) { + for (String arg : args) { + try { + File file = new File(arg); + System.out.println(FileType.valueOf(file)); + } catch (SchemaLocationException ex) { + System.out.println(ex); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java new file mode 100644 index 00000000..61d8e0bd --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import eu.digitisation.ngram.NgramModel; +import eu.digitisation.output.Browser; +import eu.digitisation.output.OutputFileSelector; +import eu.digitisation.output.Report; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.nio.charset.Charset; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/** + * + * @author R.C.C + */ +public class GUI extends JFrame { + + private static final long serialVersionUID = 1L; + private static final Color green = Color.decode("#4C501E"); + private static final Color white = Color.decode("#FAFAFA"); + private static final Color gray = Color.decode("#EEEEEE"); + // Frame components + FileSelector gtselector; + FileSelector ocrselector; + JPanel advanced; + Link info; + JPanel actions; + + /** + * Show a warning message + * + * @param message the text to be displayed + */ + public void warn(String message) { + JOptionPane.showMessageDialog(super.getRootPane(), message, "Error", + JOptionPane.ERROR_MESSAGE); + } + + /** + * Ask for confirmation + * @param message the text to be displayed + * @return true if the option has been confirmed + */ + public boolean confirm(String message) { + return JOptionPane.showConfirmDialog(super.getRootPane(), + message, message, JOptionPane.YES_NO_OPTION) + == JOptionPane.YES_OPTION; + } + + // The unique constructor + public GUI() { + init(); + } + + /** + * Build advanced options panel + * + * @param ignoreCase + * @param ignoreDiacritics + * @param ignorePunctuation + * @param compatibilty + * @param eqfile + * @return + */ + private JPanel advancedOptionsPanel(Parameters pars) { + JPanel panel = new JPanel(new GridLayout(0, 1)); + JPanel subpanel = new JPanel(new GridLayout(0, 2)); + Color fg = getForeground(); + Color bg = getBackground(); + + subpanel.setForeground(fg); + subpanel.setBackground(bg); + subpanel.add(new BooleanSelector(pars.ignoreCase, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignoreDiacritics, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignorePunctuation, fg, bg)); + subpanel.add(new BooleanSelector(pars.compatibility, fg, bg)); + + panel.setForeground(fg); + panel.setBackground(bg); + panel.setVisible(false); + panel.add(subpanel); + panel.add(new FileSelector(pars.swfile, fg, bg)); + panel.add(new FileSelector(pars.eqfile, fg, bg)); + return panel; + } + + /** + * Creates a subpanel with two actions: "show advanced options" & "generate + * report" + * + * @param gui + * @return + */ + private JPanel actionsPanel(final GUI gui, final Parameters pars) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + final JCheckBox more = new JCheckBox("Show advanced options"); + more.setForeground(getForeground()); + more.setBackground(Color.LIGHT_GRAY); + more.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Dimension dframe = gui.getSize(); + Dimension dadvanced = gui.advanced.getPreferredSize(); + if (more.isSelected()) { + gui.setSize(new Dimension(dframe.width, dframe.height + dadvanced.height)); + } else { + gui.setSize(new Dimension(dframe.width, dframe.height - dadvanced.height)); + } + gui.advanced.setVisible(more.isSelected()); + } + }); + + JButton reset = new JButton("Reset"); + reset.setForeground(getForeground()); + reset.setBackground(getBackground()); + reset.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + pars.clear(); + gui.remove(gtselector); + gui.remove(ocrselector); + gui.remove(info); + gui.remove(advanced); + gui.remove(actions); + gui.repaint(); + gui.setVisible(true); + gui.init(); + } + }); + + // Go for it! button with inverted colors + JButton trigger = new JButton("Generate report"); + trigger.setForeground(getBackground()); + trigger.setBackground(getForeground()); + trigger.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launch(pars); + } + }); + + panel.add(more, BorderLayout.WEST); + panel.add(Box.createHorizontalGlue()); + panel.add(reset, BorderLayout.CENTER); + panel.add(Box.createHorizontalGlue()); + panel.add(trigger, BorderLayout.EAST); + return panel; + } + + private void createReport(Parameters pars) throws WarningException { + try { + Batch batch = new Batch(pars.gtfile.value, pars.ocrfile.value); + Report report = new Report(batch, pars); + File outfile = pars.outfile.getValue(); + report.write(outfile); + Messages.info("Report dumped to " + outfile); + Browser.open(outfile.toURI()); + } catch (InvalidObjectException ex) { + warn(ex.getMessage()); + } catch (SchemaLocationException ex) { + boolean ans = confirm("Unknown schema location:\n" + + ex.getSchemaLocation() + + "\n\nAdd it to the list of valid schemas?"); + if (ans) { + String prop = "schemaLocation." + ex.getFileType(); + String value = ex.getSchemaLocation(); + Settings.addUserProperty(prop, value); + Messages.info(prop + " set to " + + Settings.property(prop)); + } + } catch (IOException ex) { + warn("Input/Output Error"); + } + } + + public void launch(Parameters pars) { + try { + if (ocrselector.ready() && (gtselector.ready())) { + File ocrfile = pars.ocrfile.getValue(); + String name = ocrfile.getName().replaceAll("\\.\\w+", "") + + "_report.html"; + File dir = ocrfile.getParentFile(); + File preselected = new File(name); + OutputFileSelector selector = new OutputFileSelector(); + File outfile = selector.choose(dir, preselected); + pars.outfile.setValue(outfile); + + if (outfile != null) { + createReport(pars); + } + } else { + gtselector.checkout(); + ocrselector.checkout(); + } + } catch (WarningException ex) { + warn(ex.getMessage()); + } + } + + public final void init() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // Main container + Container pane = getContentPane(); + // Initialization settings + setForeground(green); + setBackground(gray); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); + setLocationRelativeTo(null); + + // Define program parameters: input files + Parameters pars = new Parameters(); + + // Define content + gtselector = new FileSelector(pars.gtfile, getForeground(), white); + ocrselector = new FileSelector(pars.ocrfile, getForeground(), white); + advanced = advancedOptionsPanel(pars); + info = new Link("Info:", "https://sites.google.com/site/textdigitisation/ocrevaluation", getForeground()); + actions = actionsPanel(this, pars); + + // Put all content together + pane.add(gtselector); + pane.add(ocrselector); + pane.add(advanced); + pane.add(info); + pane.add(actions); + + // menu bar + JMenuBar menuBar = new JMenuBar(); + JMenu mainMenu = new JMenu("Main"); + JMenuItem createLanguageModelMenuItem = new JMenuItem("Create Language Model...", KeyEvent.VK_C); + createLanguageModelMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + File inputFile = choose("Choose file to create language model", "sample.txt"); + if (inputFile != null) { + File outputFile = choose("Choose output file", "model.lm"); + if (outputFile != null) { + Object[] possibilities = {"2", "3", "4", "5"}; + String value + = (String) JOptionPane.showInputDialog(null, "Select value vor 'n'", "", + JOptionPane.QUESTION_MESSAGE, null, possibilities, "2"); + if (value != null) { + int n = Integer.parseInt(value); + + NgramModel ngramModel = new NgramModel(n); + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + if (inputFile.isDirectory()) { + File[] files = inputFile.listFiles(); + for (File file : files) { + ngramModel.addWords(file, encoding, false); + } + } else { + ngramModel.addWords(inputFile, encoding, false); + } + ngramModel.save(outputFile); + } + } + } + } + }); + JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_X); + exitMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + + mainMenu.add(createLanguageModelMenuItem); + mainMenu.addSeparator(); + mainMenu.add(exitMenuItem); + + menuBar.add(mainMenu); + + this.setJMenuBar(menuBar); + + // Show + pack(); + setVisible(true); + } + + private File choose(String title, String defaultName) { + JFileChooser chooser = new JFileChooser(); + + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + chooser.setDialogTitle(title); + chooser.setSelectedFile(new File(defaultName)); + int returnVal = chooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + return chooser.getSelectedFile(); + } else { + return null; + } + } + + public static void main(String[] args) { + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + new GUI(); + } + }); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java~ b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java~ new file mode 100644 index 00000000..61d8e0bd --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUI.java~ @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import eu.digitisation.ngram.NgramModel; +import eu.digitisation.output.Browser; +import eu.digitisation.output.OutputFileSelector; +import eu.digitisation.output.Report; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.nio.charset.Charset; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/** + * + * @author R.C.C + */ +public class GUI extends JFrame { + + private static final long serialVersionUID = 1L; + private static final Color green = Color.decode("#4C501E"); + private static final Color white = Color.decode("#FAFAFA"); + private static final Color gray = Color.decode("#EEEEEE"); + // Frame components + FileSelector gtselector; + FileSelector ocrselector; + JPanel advanced; + Link info; + JPanel actions; + + /** + * Show a warning message + * + * @param message the text to be displayed + */ + public void warn(String message) { + JOptionPane.showMessageDialog(super.getRootPane(), message, "Error", + JOptionPane.ERROR_MESSAGE); + } + + /** + * Ask for confirmation + * @param message the text to be displayed + * @return true if the option has been confirmed + */ + public boolean confirm(String message) { + return JOptionPane.showConfirmDialog(super.getRootPane(), + message, message, JOptionPane.YES_NO_OPTION) + == JOptionPane.YES_OPTION; + } + + // The unique constructor + public GUI() { + init(); + } + + /** + * Build advanced options panel + * + * @param ignoreCase + * @param ignoreDiacritics + * @param ignorePunctuation + * @param compatibilty + * @param eqfile + * @return + */ + private JPanel advancedOptionsPanel(Parameters pars) { + JPanel panel = new JPanel(new GridLayout(0, 1)); + JPanel subpanel = new JPanel(new GridLayout(0, 2)); + Color fg = getForeground(); + Color bg = getBackground(); + + subpanel.setForeground(fg); + subpanel.setBackground(bg); + subpanel.add(new BooleanSelector(pars.ignoreCase, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignoreDiacritics, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignorePunctuation, fg, bg)); + subpanel.add(new BooleanSelector(pars.compatibility, fg, bg)); + + panel.setForeground(fg); + panel.setBackground(bg); + panel.setVisible(false); + panel.add(subpanel); + panel.add(new FileSelector(pars.swfile, fg, bg)); + panel.add(new FileSelector(pars.eqfile, fg, bg)); + return panel; + } + + /** + * Creates a subpanel with two actions: "show advanced options" & "generate + * report" + * + * @param gui + * @return + */ + private JPanel actionsPanel(final GUI gui, final Parameters pars) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + final JCheckBox more = new JCheckBox("Show advanced options"); + more.setForeground(getForeground()); + more.setBackground(Color.LIGHT_GRAY); + more.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Dimension dframe = gui.getSize(); + Dimension dadvanced = gui.advanced.getPreferredSize(); + if (more.isSelected()) { + gui.setSize(new Dimension(dframe.width, dframe.height + dadvanced.height)); + } else { + gui.setSize(new Dimension(dframe.width, dframe.height - dadvanced.height)); + } + gui.advanced.setVisible(more.isSelected()); + } + }); + + JButton reset = new JButton("Reset"); + reset.setForeground(getForeground()); + reset.setBackground(getBackground()); + reset.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + pars.clear(); + gui.remove(gtselector); + gui.remove(ocrselector); + gui.remove(info); + gui.remove(advanced); + gui.remove(actions); + gui.repaint(); + gui.setVisible(true); + gui.init(); + } + }); + + // Go for it! button with inverted colors + JButton trigger = new JButton("Generate report"); + trigger.setForeground(getBackground()); + trigger.setBackground(getForeground()); + trigger.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launch(pars); + } + }); + + panel.add(more, BorderLayout.WEST); + panel.add(Box.createHorizontalGlue()); + panel.add(reset, BorderLayout.CENTER); + panel.add(Box.createHorizontalGlue()); + panel.add(trigger, BorderLayout.EAST); + return panel; + } + + private void createReport(Parameters pars) throws WarningException { + try { + Batch batch = new Batch(pars.gtfile.value, pars.ocrfile.value); + Report report = new Report(batch, pars); + File outfile = pars.outfile.getValue(); + report.write(outfile); + Messages.info("Report dumped to " + outfile); + Browser.open(outfile.toURI()); + } catch (InvalidObjectException ex) { + warn(ex.getMessage()); + } catch (SchemaLocationException ex) { + boolean ans = confirm("Unknown schema location:\n" + + ex.getSchemaLocation() + + "\n\nAdd it to the list of valid schemas?"); + if (ans) { + String prop = "schemaLocation." + ex.getFileType(); + String value = ex.getSchemaLocation(); + Settings.addUserProperty(prop, value); + Messages.info(prop + " set to " + + Settings.property(prop)); + } + } catch (IOException ex) { + warn("Input/Output Error"); + } + } + + public void launch(Parameters pars) { + try { + if (ocrselector.ready() && (gtselector.ready())) { + File ocrfile = pars.ocrfile.getValue(); + String name = ocrfile.getName().replaceAll("\\.\\w+", "") + + "_report.html"; + File dir = ocrfile.getParentFile(); + File preselected = new File(name); + OutputFileSelector selector = new OutputFileSelector(); + File outfile = selector.choose(dir, preselected); + pars.outfile.setValue(outfile); + + if (outfile != null) { + createReport(pars); + } + } else { + gtselector.checkout(); + ocrselector.checkout(); + } + } catch (WarningException ex) { + warn(ex.getMessage()); + } + } + + public final void init() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // Main container + Container pane = getContentPane(); + // Initialization settings + setForeground(green); + setBackground(gray); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); + setLocationRelativeTo(null); + + // Define program parameters: input files + Parameters pars = new Parameters(); + + // Define content + gtselector = new FileSelector(pars.gtfile, getForeground(), white); + ocrselector = new FileSelector(pars.ocrfile, getForeground(), white); + advanced = advancedOptionsPanel(pars); + info = new Link("Info:", "https://sites.google.com/site/textdigitisation/ocrevaluation", getForeground()); + actions = actionsPanel(this, pars); + + // Put all content together + pane.add(gtselector); + pane.add(ocrselector); + pane.add(advanced); + pane.add(info); + pane.add(actions); + + // menu bar + JMenuBar menuBar = new JMenuBar(); + JMenu mainMenu = new JMenu("Main"); + JMenuItem createLanguageModelMenuItem = new JMenuItem("Create Language Model...", KeyEvent.VK_C); + createLanguageModelMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + File inputFile = choose("Choose file to create language model", "sample.txt"); + if (inputFile != null) { + File outputFile = choose("Choose output file", "model.lm"); + if (outputFile != null) { + Object[] possibilities = {"2", "3", "4", "5"}; + String value + = (String) JOptionPane.showInputDialog(null, "Select value vor 'n'", "", + JOptionPane.QUESTION_MESSAGE, null, possibilities, "2"); + if (value != null) { + int n = Integer.parseInt(value); + + NgramModel ngramModel = new NgramModel(n); + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + if (inputFile.isDirectory()) { + File[] files = inputFile.listFiles(); + for (File file : files) { + ngramModel.addWords(file, encoding, false); + } + } else { + ngramModel.addWords(inputFile, encoding, false); + } + ngramModel.save(outputFile); + } + } + } + } + }); + JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_X); + exitMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + + mainMenu.add(createLanguageModelMenuItem); + mainMenu.addSeparator(); + mainMenu.add(exitMenuItem); + + menuBar.add(mainMenu); + + this.setJMenuBar(menuBar); + + // Show + pack(); + setVisible(true); + } + + private File choose(String title, String defaultName) { + JFileChooser chooser = new JFileChooser(); + + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + chooser.setDialogTitle(title); + chooser.setSelectedFile(new File(defaultName)); + int returnVal = chooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + return chooser.getSelectedFile(); + } else { + return null; + } + } + + public static void main(String[] args) { + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + new GUI(); + } + }); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/GUIHack.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUIHack.java new file mode 100644 index 00000000..4e023bbd --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/GUIHack.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import eu.digitisation.ngram.NgramModel; +import eu.digitisation.ngram.NgramPerplexityEvaluator; +import eu.digitisation.output.Browser; +import eu.digitisation.output.OutputFileSelector; +import eu.digitisation.output.Report; +import eu.digitisation.text.Text; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/** + * + * @author R.C.C + */ +public class GUIHack extends JFrame { + + private static final long serialVersionUID = 1L; + private static final Color green = Color.decode("#4C501E"); + private static final Color white = Color.decode("#FAFAFA"); + private static final Color gray = Color.decode("#EEEEEE"); + // Frame components + FileSelector gtselector; + FileSelector ocrselector; + FileSelector lmselector; + JPanel advanced; + Link info; + JPanel actions; + + /** + * Show a warning message + * + * @param text the text to be displayed + */ + + public void warn(String message) { + JOptionPane.showMessageDialog(super.getRootPane(), message, "Error", + JOptionPane.ERROR_MESSAGE); + } + + /** + * Ask for confirmation + */ + public boolean confirm(String message) { + return JOptionPane.showConfirmDialog(super.getRootPane(), + message, message, JOptionPane.YES_NO_OPTION) + == JOptionPane.YES_OPTION; + } + + // The unique constructor + public GUIHack() { + init(); + } + + /** + * Build advanced options panel + * + * @param ignoreCase + * @param ignoreDiacritics + * @param ignorePunctuation + * @param compatibilty + * @param eqfile + * @return + */ + private JPanel advancedOptionsPanel(Parameters pars) { + JPanel panel = new JPanel(new GridLayout(0, 1)); + JPanel subpanel = new JPanel(new GridLayout(0, 2)); + Color fg = getForeground(); + Color bg = getBackground(); + + subpanel.setForeground(fg); + subpanel.setBackground(bg); + subpanel.add(new BooleanSelector(pars.ignoreCase, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignoreDiacritics, fg, bg)); + subpanel.add(new BooleanSelector(pars.ignorePunctuation, fg, bg)); + subpanel.add(new BooleanSelector(pars.compatibility, fg, bg)); + + panel.setForeground(fg); + panel.setBackground(bg); + panel.setVisible(false); + panel.add(subpanel); + panel.add(new FileSelector(pars.swfile, fg, bg)); + panel.add(new FileSelector(pars.eqfile, fg, bg)); + return panel; + } + + /** + * Creates a subpanel with two actions: "show advanced options" & "generate + * report" + * + * @param gui + * @return + */ + private JPanel actionsPanel(final GUIHack gui, final Parameters pars) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + final JCheckBox more = new JCheckBox("Show advanced options"); + more.setForeground(getForeground()); + more.setBackground(Color.LIGHT_GRAY); + more.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Dimension dframe = gui.getSize(); + Dimension dadvanced = gui.advanced.getPreferredSize(); + if (more.isSelected()) { + gui.setSize(new Dimension(dframe.width, dframe.height + dadvanced.height)); + } else { + gui.setSize(new Dimension(dframe.width, dframe.height - dadvanced.height)); + } + gui.advanced.setVisible(more.isSelected()); + } + }); + + JButton reset = new JButton("Reset"); + reset.setForeground(getForeground()); + reset.setBackground(getBackground()); + reset.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + pars.clear(); + gui.remove(gtselector); + gui.remove(ocrselector); + gui.remove(info); + gui.remove(advanced); + gui.remove(actions); + gui.repaint(); + gui.setVisible(true); + gui.init(); + } + }); + + // Go for it! button with inverted colors + JButton trigger = new JButton("Generate report"); + trigger.setForeground(getBackground()); + trigger.setBackground(getForeground()); + trigger.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + try { + launch(pars); + } catch (SchemaLocationException ex) { + Messages.severe(this.getClass() + ": "+ ex.getMessage()); + } + } + }); + + panel.add(more, BorderLayout.WEST); + panel.add(Box.createHorizontalGlue()); + panel.add(reset, BorderLayout.CENTER); + panel.add(Box.createHorizontalGlue()); + panel.add(trigger, BorderLayout.EAST); + return panel; + } + + public void launch(Parameters pars) throws SchemaLocationException { + try { + if (ocrselector.ready() && (gtselector.ready() || lmselector.ready())) { + File ocrfile = pars.ocrfile.getValue(); + if (gtselector.ready()) { + String name = ocrfile.getName().replaceAll("\\.\\w+", "") + "_report.html"; + File dir = ocrfile.getParentFile(); + File preselected = new File(name); + OutputFileSelector selector = new OutputFileSelector(); + File outfile = selector.choose(dir, preselected); + pars.outfile.setValue(outfile); + + if (outfile != null) { + try { + Batch batch = new Batch(pars.gtfile.value, pars.ocrfile.value); + Report report = new Report(batch, pars); + report.write(outfile); + Messages.info("Report dumped to " + outfile); + Browser.open(outfile.toURI()); + } catch (InvalidObjectException ex) { + warn(ex.getMessage()); + } catch (IOException ex) { + warn("Input/Output Error"); + } + } + } + if (lmselector.ready()) { + Object[] possibilities = {"2", "3", "4", "5"}; + String value + = (String) JOptionPane.showInputDialog(null, "Select contect length", "", + JOptionPane.QUESTION_MESSAGE, null, possibilities, "2"); + if (value != null) { + int contextLength = Integer.parseInt(value); + + NgramPerplexityEvaluator lpc = new NgramPerplexityEvaluator(pars.lmfile.value); + + Text ocr = new Text(ocrfile); + double[] perplexityArray = lpc.calculatePerplexity(ocr.toString(), contextLength); + + LanguageModelEvaluationFrame frame = new LanguageModelEvaluationFrame(); + frame.setInput(ocr.toString(), perplexityArray); + frame.setVisible(true); + } + } + } else { + gtselector.checkout(); + ocrselector.checkout(); + lmselector.checkout(); + } + } catch (WarningException ex) { + warn(ex.getMessage()); + } + } + + public final void init() { + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // Main container + Container pane = getContentPane(); + // Initialization settings + setForeground(green); + setBackground(gray); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); + setLocationRelativeTo(null); + + // Define program parameters: input files + Parameters pars = new Parameters(); + + // Define content + gtselector = new FileSelector(pars.gtfile, getForeground(), white); + ocrselector = new FileSelector(pars.ocrfile, getForeground(), white); + lmselector = new FileSelector(pars.lmfile, getForeground(), white); + advanced = advancedOptionsPanel(pars); + info = new Link("Info:", "https://sites.google.com/site/textdigitisation/ocrevaluation", getForeground()); + actions = actionsPanel(this, pars); + + // Put all content together + pane.add(gtselector); + pane.add(ocrselector); + pane.add(lmselector); + pane.add(advanced); + pane.add(info); + pane.add(actions); + + // menu bar + JMenuBar menuBar = new JMenuBar(); + JMenu mainMenu = new JMenu("Main"); + JMenuItem createLanguageModelMenuItem = new JMenuItem("Create Language Model...", KeyEvent.VK_C); + createLanguageModelMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + File inputFile = choose("Choose file to create language model", "sample.txt"); + if (inputFile != null) { + File outputFile = choose("Choose output file", "model.lm"); + if (outputFile != null) { + Object[] possibilities = {"2", "3", "4", "5"}; + String value + = (String) JOptionPane.showInputDialog(null, "Select value vor 'n'", "", + JOptionPane.QUESTION_MESSAGE, null, possibilities, "2"); + if (value != null) { + int n = Integer.parseInt(value); + + NgramModel ngramModel = new NgramModel(n); + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + if (inputFile.isDirectory()) { + File[] files = inputFile.listFiles(); + for (File file : files) { + ngramModel.addWords(file, encoding, false); + } + } else { + ngramModel.addWords(inputFile, encoding, false); + } + ngramModel.save(outputFile); + } + } + } + } + }); + JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_X); + exitMenuItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + + mainMenu.add(createLanguageModelMenuItem); + mainMenu.addSeparator(); + mainMenu.add(exitMenuItem); + + menuBar.add(mainMenu); + + this.setJMenuBar(menuBar); + + // Show + pack(); + setVisible(true); + } + + private File choose(String title, String defaultName) { + JFileChooser chooser = new JFileChooser(); + + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + chooser.setDialogTitle(title); + chooser.setSelectedFile(new File(defaultName)); + int returnVal = chooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + return chooser.getSelectedFile(); + } else { + return null; + } + } + + public static void main(String[] args) { + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + new GUI(); + } + }); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Help.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Help.java new file mode 100644 index 00000000..13f4022f --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Help.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import eu.digitisation.output.Browser; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Shape; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.Ellipse2D; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JButton; +import javax.swing.JOptionPane; + +/** + * Help text or URL for additional information (URL must start with http:) + * + * @author R.C.C. + */ +public class Help extends JButton { + + private static final long serialVersionUID = 1L; + String text; // help text + + /** + * Default constructor + * + * @param helpText the help text or URL + * @param forecolor foreground color + * @param bgcolor background color + */ + public Help(String helpText, Color forecolor, Color bgcolor) { + super("?"); + setPreferredSize(new Dimension(10, 10)); + setForeground(forecolor); + setBackground(bgcolor); + setContentAreaFilled(false); + + this.text = helpText; + + addActionListener(new ActionListener() { + Container container = getParent(); + + @Override + public void actionPerformed(ActionEvent e) { + if (text.startsWith("http:")) { + try { + Browser.open(new URI(text)); + } catch (URISyntaxException ex) { + Messages.severe(Help.class.getName() + ex); + } + } else { + JOptionPane.showMessageDialog(getParent(), text); + } + } + }); + } + + // Artwork + @Override + protected void paintComponent(Graphics g) { + if (getModel().isArmed()) { + g.setColor(Color.lightGray); + } else { + g.setColor(getBackground()); + } + g.fillOval(7, 0, getSize().width - 16, getSize().height - 1); + + super.paintComponent(g); + } + + @Override + protected void paintBorder(Graphics g) { + g.setColor(getForeground()); + g.drawOval(7, 0, getSize().width - 16, getSize().height - 1); + } + Shape shape; + + @Override + public boolean contains(int x, int y) { + if (shape == null + || !shape.getBounds().equals(getBounds())) { + shape = new Ellipse2D.Float(7, 0, getWidth() - 7, getHeight()); + } + return shape.contains(x, y); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/LanguageModelEvaluationFrame.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/LanguageModelEvaluationFrame.java new file mode 100644 index 00000000..7241e835 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/LanguageModelEvaluationFrame.java @@ -0,0 +1,238 @@ +package eu.digitisation.input; + +import eu.digitisation.text.Text; +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Font; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +public class LanguageModelEvaluationFrame extends JFrame { + + /** + * */ + private static final long serialVersionUID = 4895806099667768081L; + + private JPanel contentPane; + private JTextField thresholdTextField; + private JSlider thresholdSlider; + private JTextPane textPane; + + /** + * double array containing the perplexity values for every character. + */ + private double[] perplexityArray; + + /** + * text style applied when the threshold value is exceeded. + */ + private static final SimpleAttributeSet thresholdExceededStyle = new SimpleAttributeSet(); + /** + * default text style. + */ + private static final SimpleAttributeSet defaultStyle = new SimpleAttributeSet(); + + static { + StyleConstants.setForeground(thresholdExceededStyle, Color.red); + StyleConstants.setForeground(defaultStyle, Color.black); + } + + /** + * Create the frame. + */ + public LanguageModelEvaluationFrame() { + init(); + } + + /** + * GUI initialization. + */ + private void init() { + setBounds(100, 100, 473, 347); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(contentPane); + + JPanel panel = new JPanel(); + + JScrollPane scrollPane = new JScrollPane(); + GroupLayout gl_contentPane = new GroupLayout(contentPane); + gl_contentPane.setHorizontalGroup(gl_contentPane.createParallelGroup(Alignment.LEADING) + .addComponent(panel, GroupLayout.DEFAULT_SIZE, 447, Short.MAX_VALUE) + .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 447, Short.MAX_VALUE)); + gl_contentPane.setVerticalGroup(gl_contentPane.createParallelGroup(Alignment.LEADING).addGroup( + gl_contentPane + .createSequentialGroup() + .addComponent(panel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE).addPreferredGap(ComponentPlacement.RELATED) + .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE))); + + textPane = new JTextPane(); + textPane.setFont(new Font("Tahoma", Font.PLAIN, 16)); + scrollPane.setViewportView(textPane); + + thresholdTextField = new JTextField(); + thresholdTextField.setEditable(false); + thresholdTextField.setFont(new Font("Tahoma", Font.BOLD, 16)); + thresholdTextField.setText("-1"); + thresholdTextField.setColumns(10); + + thresholdSlider = new JSlider(); + thresholdSlider.setValue(-1); + thresholdSlider.setMaximum(-1); + thresholdSlider.setMinimum(-50); + thresholdSlider.addChangeListener(new ChangeListener() { + + @Override + public void stateChanged(ChangeEvent e) { + if (!thresholdSlider.getValueIsAdjusting()) { + thresholdTextField.setText((double) thresholdSlider.getValue() + ""); + update(true); + } + } + }); + GroupLayout gl_panel = new GroupLayout(panel); + gl_panel.setHorizontalGroup(gl_panel.createParallelGroup(Alignment.TRAILING).addGroup( + gl_panel.createSequentialGroup() + .addComponent(thresholdSlider, GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(thresholdTextField, GroupLayout.PREFERRED_SIZE, 74, GroupLayout.PREFERRED_SIZE) + .addContainerGap())); + gl_panel.setVerticalGroup(gl_panel.createParallelGroup(Alignment.TRAILING).addGroup( + gl_panel.createSequentialGroup() + .addContainerGap() + .addGroup( + gl_panel.createParallelGroup(Alignment.TRAILING) + .addComponent(thresholdSlider, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 31, + Short.MAX_VALUE) + .addComponent(thresholdTextField, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, + 31, Short.MAX_VALUE)).addContainerGap())); + panel.setLayout(gl_panel); + contentPane.setLayout(gl_contentPane); + + } + + /** + * update the evaluation results. + */ + private void update(boolean thresholdMode) { + Double threshold = 0.0; + try { + threshold = Double.parseDouble(thresholdTextField.getText()); + StyledDocument document = textPane.getStyledDocument(); + + document.setCharacterAttributes(0, document.getLength(), defaultStyle, true); + if (thresholdMode) { + for (int i = 0; i < perplexityArray.length; i++) { + if (perplexityArray[i] < threshold) { + document.setCharacterAttributes(i, 1, thresholdExceededStyle, true); + } + } + } else { + double max = 0; + + for (int i = 0; i < perplexityArray.length; i++) { + double value = perplexityArray[i]; + if (!Double.isInfinite(value)) { + if (Math.abs(value) > Math.abs(max)) { + max = value; + } + } + } + + List colors = getColorBands(Color.red, 11); + + for (int i = 0; i < perplexityArray.length; i++) { + SimpleAttributeSet style = new SimpleAttributeSet(); + double value = perplexityArray[i]; + if (Double.isInfinite(value)) { + StyleConstants.setForeground(style, Color.red); + } else { + int color = 10 - (int) ((value / max) * 10); + StyleConstants.setForeground(style, colors.get(color)); + } + document.setCharacterAttributes(i, 1, style, true); + } + } + + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(this, "Unable to parse value '" + thresholdTextField.getText() + "'", + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + public void setInput(String textToEvaluate, double[] perplexityArray) { + this.perplexityArray = perplexityArray; + textPane.setText(textToEvaluate); + update(false); + } + + public List getColorBands(Color color, int bands) { + + List colorBands = new ArrayList(bands); + for (int index = 0; index < bands; index++) { + colorBands.add(darken(color, (double) index / (double) bands)); + } + return colorBands; + + } + + public static Color darken(Color color, double fraction) { + + int red = (int) Math.round(Math.max(0, color.getRed() - 255 * fraction)); + int green = (int) Math.round(Math.max(0, color.getGreen() - 255 * fraction)); + int blue = (int) Math.round(Math.max(0, color.getBlue() - 255 * fraction)); + + int alpha = color.getAlpha(); + + return new Color(red, green, blue, alpha); + + } + + /** + * Launch the application. + */ + public static void main(final String[] args) { + + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + Text ocr = new Text(new File(args[0])); + final double[] perplexityArray + = new double[]{0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, + 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, + 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, + 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, + 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, + 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9}; + + LanguageModelEvaluationFrame frame = new LanguageModelEvaluationFrame(); + frame.setInput(ocr.toString(), perplexityArray); + frame.setVisible(true); + + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Link.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Link.java new file mode 100644 index 00000000..46ea7280 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Link.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + + +import eu.digitisation.log.Messages; +import eu.digitisation.output.Browser; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * + * @author R.C.C + */ +public class Link extends JPanel { + private static final long serialVersionUID = 1L; + JLabel link; + /** + * Basic constructor + * @param title the text to be shown + * @param url the linked URL + * @param color the color of the link + */ + public Link(final String title, final String url, Color color) { + setPreferredSize(new Dimension(600,30)); + link = new JLabel(); + link.setFont(new Font("Verdana", Font.PLAIN, 12)); + link.setAlignmentX(LEFT_ALIGNMENT); + link.setText("" + title + + "" + url + + ""); + link.setCursor(new Cursor(Cursor.HAND_CURSOR)); + link.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + try { + Browser.open(new URI(url)); + } catch (URISyntaxException ex) { + Messages.severe(Link.class.getName() + ex); + } + } + }); + add(link); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameter.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameter.java new file mode 100644 index 00000000..7af017ee --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameter.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +/** + * A program parameter with a value, a name (a short description) and, + * optionally, a help text or URL providing a longer description. + * + * @author R.C.C. + * @param the type of parameter (Boolean, File) + */ +public class Parameter { + + String name; + Type value; + String help; // text help or URL + + /** + * Crete a Parameter with the given name (and null value) + * + * @param name the parameter's name + */ + Parameter(String name) { + this.name = name; + } + + /** + * Create Parameter with the given name and set this parameter's help text + * and URL with additional help + * + * @param name the parameter's name + * @param value this parameter's value + * @param help help text or URL for this parameter + */ + Parameter(String name, Type value, String help) { + this.name = name; + this.value = value; + this.help = help; + } + + /** + * Set this parameter's value + * + * @param value the parameter's value + */ + public void setValue(Type value) { + this.value = value; + } + + /** + * Get this parameter's value + * + * @return the parameter's value + */ + public Type getValue() { + return value; + } + + /** + * Get the parameter value type (Boolean, File, Integer,...) + * + * @return + */ + public Class getType() { + return value.getClass(); + } + + /** + * + * @returna short description of the parameter + */ + public String getName() { + return name; + } + + /** + * + * @return the help text for this parameter + */ + public String getHelp() { + return help; + } + + /** + * + * @return a string name:value + */ + @Override + public String toString() { + return name + ":" + value; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/ParameterSelector.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/ParameterSelector.java new file mode 100644 index 00000000..615caf74 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/ParameterSelector.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.awt.Color; +import javax.swing.JPanel; + +/** + * + * @author R.C.C + * @param the type of parameter (Boolean, File, ...) + */ +public abstract class ParameterSelector extends JPanel { + private static final long serialVersionUID = 1L; + + Parameter param; + + public ParameterSelector(Parameter param, Color forecolor, Color backcolor) { + this.param = param; + setForeground(forecolor); + setBackground(backcolor); + } + + public Parameter getOption() { + return param; + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameters.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameters.java new file mode 100644 index 00000000..8e56f85c --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Parameters.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.io.File; + +/** + * Stores all the input parameters used by the program + * + * @author R.C.C. + */ +public class Parameters { + + private static final long serialVersionUID = 1L; + // Define program parameters: input files + public final Parameter gtfile; + public final Parameter ocrfile; + public final Parameter eqfile; // equivalences + public final Parameter swfile; // stop words + public final Parameter lmfile; // language model + public final Parameter outfile; + // Define program parameters: boolean options + public final Parameter ignoreCase; + public final Parameter ignoreDiacritics; + public final Parameter ignorePunctuation; + public final Parameter compatibility; + // Define program parameters: String options + public final Parameter encoding; + // Set verbosity during debugging (unused) + public final Parameter verbose; + + public Parameters() { + gtfile = new Parameter("ground-truth file"); + ocrfile = new Parameter("OCR file"); + eqfile = new Parameter("Unicode equivalences file"); + swfile = new Parameter("stop-words file"); + lmfile = new Parameter("Language model file"); + outfile = new Parameter("output file"); + ignoreCase = new Parameter("Ignore case", false, ""); + ignoreDiacritics = new Parameter("Ignore diacritics", false, ""); + ignorePunctuation = new Parameter("Ignore punctuation", false, ""); + compatibility = new Parameter("Unicode compatibility characters", false, + "http://unicode.org/reports/tr15/#Canon_Compat_Equivalence"); + encoding = new Parameter("Text file encoding"); + verbose = new Parameter("Verbose", false, ""); + } + + public void clear() { + gtfile.setValue(null); + ocrfile.setValue(null); + eqfile.setValue(null); + swfile.setValue(null); + lmfile.setValue(null); + outfile.setValue(null); + ignoreCase.setValue(null); + ignoreDiacritics.setValue(null); + ignorePunctuation.setValue(null); + compatibility.setValue(null); + encoding.setValue(null); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/SchemaLocationException.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/SchemaLocationException.java new file mode 100644 index 00000000..0b251f23 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/SchemaLocationException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import java.io.IOException; + +/** + * An exception raised because the schema for the XML file is unknown + * @author R.C.C + */ + +public class SchemaLocationException extends IOException { + FileType type; + String schemaLocation; + + SchemaLocationException(FileType type, String schemaLocation) { + this.type = type; + this.schemaLocation = schemaLocation; + } + + public FileType getFileType() { + return type; + } + + public String getSchemaLocation() { + return schemaLocation; + } + + @Override + public String toString() { + return "Unknown schema location " + schemaLocation + + " for file type " + type; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/Settings.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/Settings.java new file mode 100644 index 00000000..ac7fe961 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/Settings.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.log.Messages; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Properties; + +/** + * Start-up actions: load default and user properties (user-defined values + * overwrite defaults). + * + * @author R.C.C. + */ +public class Settings { + + private static Properties props = new Properties(); + + /** + * Get application directory + */ + private static File appDir() { + try { + URI uri = Messages.class.getProtectionDomain() + .getCodeSource().getLocation().toURI(); + File dir = new File(uri.getPath()).getParentFile(); + + Messages.info("Application folder is " + dir); + return dir; + } catch (URISyntaxException ex) { + Messages.severe(Settings.class.getName() + ": " + ex); + } + return null; + } + + static { + try { + InputStream in; + // Read defaults + Properties defaults = new Properties(); + in = Settings.class.getResourceAsStream("/defaultProperties.xml"); + if (in != null) { + defaults.loadFromXML(in); + in.close(); + props = new Properties(defaults); + } + + // Add user properties (may overwrite defaults) + File file = new File(appDir(), "userProperties.xml"); + if (file.exists()) { + in = new FileInputStream(file); + props.loadFromXML(in); + Messages.info("Read properties from " + file); + in.close(); + } else { + in = Settings.class.getResourceAsStream("/userProperties.xml"); + if (in != null) { + defaults.loadFromXML(in); + Messages.info("Read properties from " + file); + in.close(); + props = new Properties(defaults); + } else { + Messages.info("No properties were defined by user"); + } + } + } catch (IOException ex) { + Messages.severe(Settings.class.getName() + ": " + ex); + } + } + + /** + * @return the properties defined at startup (user-defined overwrite + * defaults). + */ + public static Properties properties() { + return props; + } + + /** + * + * @param key a property name + * @return the property with the specified key as defined by the user, and + * otherwise, its default value ( (if the default is not defined, then the + * method returns null). + */ + public static String property(String key) { + return props.getProperty(key); + } + + /** + * Add a new value to property + * + * @param type + * @param schemaLocation + */ + public static void addUserProperty2(FileType type, String schemaLocation) { + String prop = props.getProperty("schemaLocation." + type); + String value = props.getProperty(prop); + props.setProperty(prop, value + " " + schemaLocation); + saveToFile(); + } + + /** + * Add a new value to property + * + * @param type + * @param schemaLocation + */ + public static void addUserProperty(String prop, String value) { + String currentValue = props.getProperty(prop); + if (currentValue == null) { + props.setProperty(prop, value); + } else { + props.setProperty(prop, currentValue + " " + value); + } + saveToFile(); + } + + /** + * Save properties to XML file (userProperties.xml) + */ + private static void saveToFile() { + try { + File file = new File(appDir(), "userProperties.xml"); + OutputStream os = new FileOutputStream(file); + props.storeToXML(os, null); + os.close(); + Messages.info("Created new file: " + file.getAbsolutePath()); + FileType.reload(); + } catch (IOException ex) { + Messages.severe(Settings.class.getName() + ": " + ex); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/input/WarningException.java b/ocrevalUAtion/src/main/java/eu/digitisation/input/WarningException.java new file mode 100644 index 00000000..8cb1073c --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/input/WarningException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +/** + * Exceptions which only generate a warning and waits for user reaction + * @author R.C.C. + */ +public class WarningException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * Default constructor + * @param message + */ + public WarningException(String message) { + super(message); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/langutils/TermFrequency.java b/ocrevalUAtion/src/main/java/eu/digitisation/langutils/TermFrequency.java new file mode 100644 index 00000000..e943599d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/langutils/TermFrequency.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.langutils; + +import eu.digitisation.input.SchemaLocationException; +import eu.digitisation.input.WarningException; +import eu.digitisation.log.Messages; +import eu.digitisation.math.Counter; +import eu.digitisation.text.CharFilter; +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.text.Text; +import eu.digitisation.text.WordScanner; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Compute term frequencies in a collection + * + * @author R.C.C + */ +public class TermFrequency extends Counter { + + private static final long serialVersionUID = 1L; + private CharFilter filter; + + /** + * Default constructor + */ + public TermFrequency() { + filter = null; + } + + /** + * Basic constructor + * + * @param filter a CharFilter implementing character equivalences + */ + public TermFrequency(CharFilter filter) { + this.filter = filter; + } + + /** + * Add CharFilter + * + * @param file a CSV file with character equivalences + */ + public void addFilter(File file) { + if (filter == null) { + filter = new CharFilter(file); + } else { + filter.addFilter(file); + } + } + + /** + * Extract words from a file + * + * @param dir the input file or directory + * @throws eu.digitisation.input.WarningException + * @throws eu.digitisation.input.SchemaLocationException + */ + public void add(File dir) throws WarningException, SchemaLocationException { + if (dir.isDirectory()) { + addFiles(dir.listFiles()); + } else { + File[] files = {dir}; + addFiles(files); + } + } + + /** + * Extract words from a file + * + * @param file an input files + * @throws eu.digitisation.input.WarningException + * @throws eu.digitisation.input.SchemaLocationException + */ + public void addFile(File file) throws WarningException, SchemaLocationException { + try { + Text content = new Text(file); + WordScanner scanner = new WordScanner(content.toString(), + "[^\\p{Space}]+"); + String word; + while ((word = scanner.nextWord()) != null) { + String filtered = (filter == null) + ? word : filter.translate(word); + inc(StringNormalizer.composed(filtered)); + } + } catch (IOException ex) { + Messages.info(TermFrequency.class.getName() + ": " + ex); + } + + } + + /** + * Extract words from a file + * + * @param files an array of input files + */ + private void addFiles(File[] files) throws WarningException, SchemaLocationException { + for (File file : files) { + addFile(file); + } + } + + /** + * + * @param other another term frequency vector + * @return the cosine distance (normalized scalar product) + */ + public double cosine(TermFrequency other) { + double norm1 = 0; + double norm2 = 0; + double scalar = 0; + + for (Map.Entry entry : this.entrySet()) { + int freq = entry.getValue(); + norm1 += freq * freq; + scalar += freq * other.get(entry.getKey()); + } + + for (int freq2 : other.values()) { + norm2 += freq2 * freq2; + } + + return scalar / Math.sqrt(norm1 * norm2); + } + + /** + * + * @param other another term frequency vector + * @return the recall provided by this term frequency vector (rate of words + * in the other TF matching one in this TF) + */ + public double recall(TermFrequency other) { + int total = 0; + int matched = 0; + + for (Map.Entry entry : other.entrySet()) { + total += entry.getValue(); + if (this.containsKey(entry.getKey())) { + ++matched; + } + } + + return matched / (double) total; + } + + /** + * Compute the order-independent edit-distance between two documents + * (equivalent to a bags of words model). + * + * @param other another TermFrequency + * @return the number of differences between this and the other bag of words + */ + public int editDistance(TermFrequency other) { + int dplus = 0; // excess + int dminus = 0; // fault + for (String word : this.keySet()) { + int delta = this.value(word) - other.value(word); + if (delta > 0) { + dplus += delta; + } else { + dminus += delta; + } + } + for (String word : other.keySet()) { + if (!this.containsKey(word)) { + int delta = this.value(word) - other.value(word); + if (delta > 0) { + dplus += delta; + } else { + dminus += delta; + } + } + } + return Math.max(dplus, dminus); + } + + /** + * String representation + * + * @param order the criteria to sort words + * @return String representation + */ + public String toString(Order order) { + StringBuilder builder = new StringBuilder(); + for (String word : this.keyList(order)) { + builder.append(word).append(' ') + .append(get(word)).append('\n'); + } + return builder.toString(); + } + + /** + * Main function + * + * @param args see help + * @throws java.lang.Exception + */ + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Usage: TermFrequency [-e equivalences_file] [-c] input_files_or_directories"); + } else { + TermFrequency tf = new TermFrequency(); + List files = new ArrayList(); + CharFilter filter = new CharFilter(); + for (int n = 0; n < args.length; ++n) { + if (args[n].equals("-e")) { + tf.addFilter(new File(args[++n])); + } else if (args[n].equals("-c")) { + filter.setCompatibility(true); + } else { + files.add(new File(args[n])); + } + } + for (File file : files) { + tf.add(file); + } + System.out.println(tf.toString(Order.DESCENDING_VALUE)); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/ALTOPage.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ALTOPage.java new file mode 100644 index 00000000..4bd6505d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ALTOPage.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import eu.digitisation.log.Messages; +import eu.digitisation.xml.DocumentParser; +import java.awt.Polygon; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * + * @author R.C.C. + */ +public class ALTOPage extends Page { + + public ALTOPage(File file) { + try { + parse(file); + } catch (IOException ex) { + Messages.info(ALTOPage.class.getName() + ": " + ex); + } + } + + /** + * Read the bounding-box information stored in the element attributes l, t, + * b, r + * + * @param e the container element + * @return the BoundingBox for this element or null if some attributes are + * missing + */ + private static BoundingBox getBBox(Element e) { + if (e.hasAttribute("HEIGHT") + && e.hasAttribute("WIDTH") + && e.hasAttribute("VPOS") + && e.hasAttribute("HPOS")) { + int height = Integer.parseInt(e.getAttribute("HEIGHT")); + int width = Integer.parseInt(e.getAttribute("WIDTH")); + int y0 = Integer.parseInt(e.getAttribute("VPOS")); + int x0 = Integer.parseInt(e.getAttribute("HPOS")); + return new BoundingBox(x0, y0, x0 + width, y0 + height); + } else { + return null; + } + } + + @Override + public final void parse(File file) throws IOException { + Document doc = DocumentParser.parse(file); + String id = ""; + ComponentType type = ComponentType.PAGE; + + NodeList nodes = doc.getElementsByTagName("Page"); + if (nodes.getLength() > 1) { + throw new IOException("Multiple pages in document"); + } else { + Element page = (Element) nodes.item(0); + root = parse(page); + } + + } + + /** + * Parse an XML element and retrieve the associated text component + * + * @param element an XML element + * @return the text component associated with this element + */ + TextComponent parse(Element element) { + String id = element.getAttribute("ID"); + ComponentType type = ComponentType.valueOf(FileType.ALTO, element.getTagName()); + String subtype = element.getAttribute("TYPE"); + String content = element.getAttribute("CONTENT"); + BoundingBox bbox = getBBox(element); + Polygon frontier = (bbox == null) ? null : bbox.asPolygon(); + List array = new ArrayList(); + NodeList children = element.getChildNodes(); + int number = components.size(); + + components.add(null); // reserve the place for this component + + for (int nchild = 0; nchild < children.getLength(); ++nchild) { + Node child = children.item(nchild); + if (child instanceof Element) { + Element e = (Element) child; + String tag = e.getTagName(); + if (tag.equals("PrintSpace") + || tag.equals("ComposedBlock") + || tag.equals("TextBlock") + || tag.equals("TextLine") + || tag.equals("String")) { + TextComponent subcomponent = parse(e); + array.add(subcomponent); + } + } + } + TextComponent component = new TextComponent(id, type, subtype, content, frontier); + components.set(number, component); + subcomponents.put(component, array); + return component; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/BoundingBox.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/BoundingBox.java new file mode 100644 index 00000000..bf64bb13 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/BoundingBox.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import java.awt.Polygon; +import java.awt.Rectangle; + +/** + * Bounding box = coordinates of the rectangular border that fully encloses the + * digital image + * + * @author R.C.C. + */ +public class BoundingBox extends Rectangle { + + private static final long serialVersionUID = 1L; + + /** + * Creates an empty BoundingBox, that is a rectangle with coordinates + * (x0, y0, x1, y1) = (0, 0, 0, 0) + */ + public BoundingBox() { + super(); + } + + /** + * Create a bounding box with the specified corners + * + * @param x0 upper left corner x-coordinate + * @param y0 upper-left corner y-coordinate + * @param x1 lower-right corner x-coordinate + * @param y1 lower-right corner y-coordinate + */ + public BoundingBox(int x0, int y0, int x1, int y1) { + super(x0, y0, x1 - x0, y1 - y0); + } + + /** + * Build a bounding box for a polygon + * + * @param polygon + */ + public BoundingBox(Polygon polygon) { + super(polygon.getBounds()); + } + + /** + * The bounding box a s a polygon + * + * @return + */ + public Polygon asPolygon() { + Polygon polygon = new Polygon(); + polygon.addPoint(x, y); + polygon.addPoint(x, y + height); + polygon.addPoint(x + width, y + height); + polygon.addPoint(x + width, y); + return polygon; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentTag.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentTag.java new file mode 100644 index 00000000..fa80fb42 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentTag.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; + +/** + * The tag of a text component in a document + * + * @author R.C.C. Info about tags FR10 + * http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml ALTO + * http://www.loc.gov/standards/alto/techcenter/layout.php hOCR + * http://docs.google.com/View?docid=dfxcv4vc_67g844kf + */ +public enum ComponentTag { + + PAGE_Page, PAGE_TextRegion, PAGE_TextLine, PAGE_Word, + HOCR_ocr_page, HOCR_ocr_carea, HOCR_ocr_par, + HOCR_ocr_line, HOCR_ocr_word, HOCR_ocrx_word, + FR10_page, FR10_block, FR10_text, FR10_par, FR10_line, + FR10_formatting, FR10_word, + ALTO_Page, ALTO_PrintSpace, ALTO_ComposedBlock, ALTO_TextBlock, + ALTO_TextLine, ALTO_String; + + /** + * The type for a given tag and type of file + * + * @param ftype the type of file (PAGE, hOCR, ALTO, etc) + * @param tag the component tag (TextLine, ocr_par, etc) + * @return the component type associated to this tag and type of file + */ + public static ComponentTag valueOf(FileType ftype, String tag) { + return valueOf(ftype.toString() + "_" + tag); + } + + /** + * Return the component tag without the file-type prefix + * + * @param tag a ComponentTag + * @return the tag for this component without the file-type prefix + */ + public static String shortTag(ComponentTag tag) { + return tag.toString().replaceFirst("[^_]+_", ""); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentType.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentType.java new file mode 100644 index 00000000..3bc97223 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/ComponentType.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import java.util.EnumMap; + +/** + * Types of text components in a document + * + * @author R.C.C. + */ +public enum ComponentType { + + PAGE, BLOCK, LINE, WORD, OTHER; + + final static EnumMap types + = new EnumMap(ComponentTag.class); + + static { + types.put(ComponentTag.PAGE_Page, PAGE); + types.put(ComponentTag.PAGE_TextRegion, BLOCK); + types.put(ComponentTag.PAGE_TextLine, LINE); + types.put(ComponentTag.PAGE_Word, WORD); + types.put(ComponentTag.HOCR_ocr_page, PAGE); + types.put(ComponentTag.HOCR_ocr_carea, OTHER); // page content-area + types.put(ComponentTag.HOCR_ocr_par, BLOCK); + types.put(ComponentTag.HOCR_ocr_line, LINE); + types.put(ComponentTag.HOCR_ocr_word, WORD); + types.put(ComponentTag.HOCR_ocrx_word, WORD); + types.put(ComponentTag.FR10_page, PAGE); + types.put(ComponentTag.FR10_block, BLOCK); + types.put(ComponentTag.FR10_text, OTHER); // text in block + types.put(ComponentTag.FR10_par, LINE); + types.put(ComponentTag.FR10_line, LINE); + types.put(ComponentTag.FR10_formatting, OTHER); // text in line + types.put(ComponentTag.FR10_word, WORD); + types.put(ComponentTag.ALTO_Page, PAGE); + types.put(ComponentTag.ALTO_PrintSpace, OTHER); // page main area + types.put(ComponentTag.ALTO_ComposedBlock, OTHER); + types.put(ComponentTag.ALTO_TextBlock, BLOCK); + types.put(ComponentTag.ALTO_TextLine, LINE); + types.put(ComponentTag.ALTO_String, WORD); + } + + public static ComponentType valueOf(ComponentTag tag) { + return types.get(tag); + } + + /** + * + * @param ftype the type of files + * @param tag the tag of the component + * @return the component type for this tag and type of file + */ + public static ComponentType valueOf(FileType ftype, String tag) { + return types.get(ComponentTag.valueOf(ftype, tag)); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/FR10Page.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/FR10Page.java new file mode 100644 index 00000000..3d2b6f41 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/FR10Page.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import eu.digitisation.log.Messages; +import eu.digitisation.xml.DocumentParser; +import java.awt.Polygon; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * FR10Page information contained in one GT or OCR file. Pending: store nested + * structure + * + * @author R.C.C. + */ +public class FR10Page extends Page { + + public FR10Page(File file) { + try { + parse(file); + } catch (IOException ex) { + Messages.info(FR10Page.class.getName() + ": " + ex); + } + + } + + /** + * Read the bounding-box information stored in the element attributes l, t, + * b, r + * + * @param e the container element + * @return the BoundingBox for this element or null if some attributes are + * missing + */ + private static BoundingBox getBBox(Element e) { + if (e.hasAttribute("l") + && e.hasAttribute("t") + && e.hasAttribute("r") + && e.hasAttribute("b")) { + int x0 = Integer.parseInt(e.getAttribute("l")); + int y0 = Integer.parseInt(e.getAttribute("t")); + int x1 = Integer.parseInt(e.getAttribute("r")); + int y1 = Integer.parseInt(e.getAttribute("b")); + return new BoundingBox(x0, y0, x1, y1); + } else { + return null; + } + } + + /** + * Get the textual content under a given element + * + * @param e the container element + * @return the text contained under this element + */ + private static String getTextContent(Element e) { + StringBuilder builder = new StringBuilder(); + NodeList nodes = e.getElementsByTagName("charParams"); + if (nodes.getLength() > 0) { + int last = 0; // recognise new line + for (int nchar = 0; nchar < nodes.getLength(); ++nchar) { + Element charParam = (Element) nodes.item(nchar); + String content = charParam.getTextContent(); + int left = Integer.parseInt(charParam.getAttribute("l")); + if (left < last) { + builder.append('\n'); + } + last = left; + builder.append(content.length() > 0 ? content : ' '); + } + } else if (!e.getNodeName().equals("formatting")) { + nodes = e.getElementsByTagName("formatting"); + for (int nline = 0; nline < nodes.getLength(); ++nline) { + Element charParam = (Element) nodes.item(nline); + String content = charParam.getTextContent(); + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(content); + } + } else { // a plain formatting element + builder.append(e.getTextContent()); + } + + return builder.toString(); + } + + @Override + public final void parse(File file) throws IOException { + + Document doc = DocumentParser.parse(file); + String id = ""; + ComponentType type = ComponentType.PAGE; + NodeList nodes = doc.getElementsByTagName("page"); + if (nodes.getLength() > 1) { + throw new IOException("Multiple pages in document"); + } else { + Element page = (Element) nodes.item(0); + root = parse(page); + } + } + + private TextComponent parse(Element element) { + String id = element.getAttribute("pageElemId"); + ComponentType type = ComponentType.valueOf(FileType.FR10, element.getTagName()); + String subtype = element.getAttribute("blockType"); // empty for non-blocks + String content = getTextContent(element); + BoundingBox bbox = getBBox(element); + Polygon frontier = (bbox == null) ? null : bbox.asPolygon(); + + List array = new ArrayList(); + NodeList children = element.getChildNodes(); + int number = components.size(); + + components.add(null); // add room for this component + + for (int nchild = 0; nchild < children.getLength(); ++nchild) { + Node child = children.item(nchild); + if (child instanceof Element) { + Element subelement = (Element) child; + String tag = subelement.getTagName(); + + if (tag.equals("block") + || tag.equals("text") + || tag.equals("par") + || tag.equals("line")) { + TextComponent subcomponent = parse(subelement); + array.add(subcomponent); + } else if (tag.equals("formatting")) { + List words = parseWords(subelement); + array.addAll(words); + } + } + } + + TextComponent component = new TextComponent(id, type, subtype, + content, frontier); + components.set(number, component); + subcomponents.put(component, array); + + return component; + } + + /** + * Polygonal cover of a sequence of bounding boxes + * @param points + * @return + */ + private Polygon cover(Polygon points) { + Polygon cover = new Polygon(); + for (int n = 0; n < points.npoints; ++n) { + if (n % 2 == 0) { + cover.addPoint(points.xpoints[n], points.ypoints[n]); + } else { + cover.addPoint(points.xpoints[n], points.ypoints[n - 1]); + } + } + for (int n = points.npoints - 1; n >= 0; --n) { + if (n % 2 == 1) { + cover.addPoint(points.xpoints[n], points.ypoints[n]); + } else { + cover.addPoint(points.xpoints[n], points.ypoints[n + 1]); + } + } + return cover; + } + + /** + * Specific function to split FR10 formatting elements into smaller + * components (words). Formatting is a sequence of charParams elements + * containing characters. An empty charParams element indicates word + * boundaries. + * + * @param element a formatting element + * @return the TextComponents (words) in this element + */ + private List parseWords(Element element) { + List words = new ArrayList(); + NodeList charParams = element.getElementsByTagName("charParams"); + StringBuilder builder = new StringBuilder(); + Polygon points = new Polygon(); + + for (int nchar = 0; nchar < charParams.getLength(); ++nchar) { + Element charParam = (Element) charParams.item(nchar); + String content = charParam.getTextContent().trim(); + if (!content.matches("\\p{Space}*")) { // end of word + int x0 = Integer.parseInt(charParam.getAttribute("l")); + int y0 = Integer.parseInt(charParam.getAttribute("t")); + int x1 = Integer.parseInt(charParam.getAttribute("r")); + int y1 = Integer.parseInt(charParam.getAttribute("b")); + points.addPoint(x0, y0); + points.addPoint(x1, y1); + builder.append(content); + } else if (builder.length() > 0) { // some content + TextComponent word + = new TextComponent(null, ComponentType.WORD, null, + builder.toString(), cover(points)); + words.add(word); + builder = new StringBuilder(); + points = new Polygon(); + } + } + // flush + if (builder.length() > 0) { + TextComponent word + = new TextComponent(null, ComponentType.WORD, null, + builder.toString(), cover(points)); + words.add(word); + } + components.addAll(words); + return words; + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/HOCRPage.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/HOCRPage.java new file mode 100644 index 00000000..8b782315 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/HOCRPage.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import eu.digitisation.log.Messages; +import java.awt.Polygon; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * @author R.C.C. + */ +public class HOCRPage extends Page { + + /** + * Basic constructor + * + * @param file + */ + public HOCRPage(File file) { + try { + parse(file); + } catch (IOException ex) { + Messages.info(HOCRPage.class.getName() + ": " + ex); + } + } + + @Override + public final void parse(File file) throws IOException { + org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parse(file, null); + + String id = ""; + ComponentType type = ComponentType.PAGE; + org.jsoup.select.Elements pages = doc.body().select("*[class=ocr_page"); + + if (pages.size() > 1) { + throw new IOException("Multiple pages in document"); + } else { + org.jsoup.nodes.Element page = pages.first(); + root = parse(page); + } + + } + + /** + * Parse an XML element and retrieve the associated text component + * + * @param element an HTML element + * @return the text component associated with this element + */ + TextComponent parse(org.jsoup.nodes.Element element) { + String id = element.attr("id"); + String subtype = element.attr("class"); + ComponentType type = ComponentType.valueOf(FileType.HOCR, subtype); + String content = element.text(); + Polygon frontier = null; + List array = new ArrayList(); + + // extract coordinates + String title = element.attr("title").trim(); + if (title.contains("bbox")) { + int pos = title.indexOf("bbox"); + String[] coords = title.substring(pos).split("\\p{Space}+"); + int x0 = Integer.parseInt(coords[1]); + int y0 = Integer.parseInt(coords[2]); + int x1 = Integer.parseInt(coords[3]); + int y1 = Integer.parseInt(coords[4]); + + frontier = new BoundingBox(x0, y0, x1, y1).asPolygon(); + } else if (title.contains("poly")) { + int pos = title.indexOf("poly"); + String[] coords = title.substring(pos).split("\\p{Space}+"); + int n = 1; + frontier = new Polygon(); + while (n + 1 < coords.length && !coords[n].equals(";")) { + int x = Integer.parseInt(coords[n]); + int y = Integer.parseInt(coords[n + 1]); + frontier.addPoint(x, y); + n += 2; + } + } + // get subcomponents + + org.jsoup.select.Elements children = element.children(); + + for (org.jsoup.nodes.Element child : children) { + if (child.hasAttr("class")) { + String cat = child.attr("class"); + if (cat.equals("ocr_carea") + || cat.equals("ocr_par") + || cat.equals("ocr_line") + || cat.equals("ocr_word") + || cat.equals("ocrx_word")) { + TextComponent subcomponent = parse(child); + array.add(subcomponent); + } + } + } + + TextComponent component = new TextComponent(id, type, subtype, content, frontier); + components.add(component); + System.out.println(component); + subcomponents.put(component, array); + return component; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/PAGEPage.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/PAGEPage.java new file mode 100644 index 00000000..ac168fb4 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/PAGEPage.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import eu.digitisation.log.Messages; +import eu.digitisation.xml.DocumentParser; +import java.awt.Polygon; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * + * @author R.C.C. + */ +public class PAGEPage extends Page { + + public PAGEPage(File file) { + try { + parse(file); + } catch (IOException ex) { + Messages.info(PAGEPage.class.getName() + ": " + ex); + } + } + + @Override + public final void parse(File file) throws IOException { + Document doc = DocumentParser.parse(file); + String id = doc.getDocumentElement().getAttribute("pcGtsId"); + ComponentType type = ComponentType.PAGE; + + NodeList nodes = doc.getElementsByTagName("Page"); + if (nodes.getLength() > 1) { + throw new IOException("Multiple pages in document"); + } else { + Element page = (Element) nodes.item(0); + root = parse(page); + } + + } + + /** + * Parse an XML element and retrieve the associated text component + * + * @param element an XML element + * @return the text component associated with this element + */ + TextComponent parse(Element element) { + String id = element.getAttribute("id"); + ComponentType type = + ComponentType.valueOf(FileType.PAGE, element.getTagName()); + String subtype = element.getAttribute("type"); + String content = null; + Polygon frontier = new Polygon(); + List array = new ArrayList(); + NodeList children = element.getChildNodes(); + int number = components.size(); + + components.add(null); // reserve the place for this component + + for (int nchild = 0; nchild < children.getLength(); ++nchild) { + Node child = children.item(nchild); + if (child instanceof Element) { + Element e = (Element) child; + String tag = e.getTagName(); + if (tag.equals("TextEquiv")) { + if (content != null) { + throw new DOMException(DOMException.INVALID_ACCESS_ERR, + "Multiple content in region " + id); + } + content = child.getTextContent().trim(); + } + if (tag.equals("Coords")) { + Element coords = (Element) child; + if (frontier.npoints > 0) { + throw new DOMException(DOMException.INVALID_ACCESS_ERR, + "Multiple Coords in region " + id); + } + NodeList nodes = coords.getChildNodes(); // points + for (int npoint = 0; npoint < nodes.getLength(); ++npoint) { + Node node = nodes.item(npoint); + if (node.getNodeName().equals("Point")) { + Element point = (Element) node; + int x = Integer.parseInt(point.getAttribute("x")); + int y = Integer.parseInt(point.getAttribute("y")); + frontier.addPoint(x, y); + } + } + } else if (tag.equals("TextRegion") + || tag.equals("TextLine") + || tag.equals("Word")) { + TextComponent subcomponent = parse(e); + array.add(subcomponent); + } + } + } + TextComponent component = new TextComponent(id, type, subtype, content, frontier); + components.set(number, component); + subcomponents.put(component, array); + return component; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/Page.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/Page.java new file mode 100644 index 00000000..58d88a3b --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/Page.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import eu.digitisation.input.SchemaLocationException; +import java.awt.Polygon; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Interface to process input files and extract content and geometry + * + * @author R.C.C. + */ +public abstract class Page { + + TextComponent root; // the main component + List components; // all components + Map> subcomponents; // contained components + + /** + * The basic constructor + */ + Page() { + components = new ArrayList(); + subcomponents = new HashMap>(); + } + + /** + * Parse an input file + * + * @param file the input (XML or HTML) file + */ + public abstract void parse(File file) throws IOException; + + /** + * Get all the components in this document + * + * @return all the components in this document + */ + public List getComponents() { + return components; + } + + /** + * Get all subcomponents of a given component + * + * @param component a component of the document being parsed + * @return all subcomponents of this component + */ + public List getComponents(TextComponent component) { + return subcomponents.get(component); + } + + /** + * List only components of a given type + * + * @param type a component type + * @return the list of components with this type + */ + public List getComponents(ComponentType type) { + List list = new ArrayList(); + for (TextComponent component : components) { + if (component.getType() == type) { + list.add(component); + } + } + return list; + } + + /** + * List subcomponents of a given type + * + * @param type a component type + * @return the list of components with this type + */ + public List getComponents(TextComponent component, ComponentType type) { + List list = new ArrayList(); + for (TextComponent subcomponent : subcomponents.get(component)) { + if (subcomponent.getType() == type) { + list.add(subcomponent); + } + } + return list; + } + + /** + * Get the textual content of the document + * + * @return the textual content of the document + */ + public String getText() { + return root.getContent(); + } + + /** + * Get the text content of a given component + * + * @param component a component of the document being parsed + * @return text under this component + */ + public String getText(TextComponent component) { + return component.getContent(); + } + + /** + * Get the text content of all components of a given type + * + * @param type a component type + * @return text content under components of this type + */ + public String getText(ComponentType type) { + StringBuilder builder = new StringBuilder(); + for (TextComponent component : components) { + if (component.getType() == type) { + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(component.getContent()); + } + } + return builder.toString(); + } + + /** + * Text content in subcomponents of a given type + * + * @param component a component + * @param type a component type + * @return the text content under subcomponents with this type + */ + public String getText(TextComponent component, ComponentType type) { + StringBuilder builder = new StringBuilder(); + for (TextComponent subcomponent : subcomponents.get(component)) { + if (subcomponent.getType() == type) { + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(subcomponent.getContent()); + } + } + return builder.toString(); + } + + /** + * Transform a list of components into a list of polygonal frontiers + * + * @param components + * @return + */ + private List frontiers(List components) { + List frontiers = new ArrayList(components.size()); + for (TextComponent component : components) { + frontiers.add(component.getFrontier()); + } + return frontiers; + } + + /** + * Get all the components in this document + * + * @return all the components in this document + */ + public List getFrontiers() { + return frontiers(components); + } + + /** + * Get the frontier a given component + * + * @param component a component of the document being parsed + * @return all subcomponents of this component + */ + public Polygon getFrontier(TextComponent component) { + return component.getFrontier(); + } + + /** + * List of (non-null) frontiers of components with a given type + * + * @param type a component type + * @return the list of polygonal frontiers of components with this type + */ + public List getFrontiers(ComponentType type) { + List list = new ArrayList(); + for (TextComponent component : components) { + if (component.getType() == type) { + Polygon p = component.getFrontier(); + if (p != null) { + list.add(p); + } + } + } + return list; + } + + /** + * List (non-null) frontiers of subcomponents with a given type + * + * @param type a component type + * @return the list of frontiers of subcomponents with this type + */ + public List getFrontiers(TextComponent component, ComponentType type) { + List list = new ArrayList(); + for (TextComponent subcomponent : subcomponents.get(component)) { + if (subcomponent.getType() == type) { + Polygon p = subcomponent.getFrontier(); + if (p != null) { + list.add(p); + } + list.add(subcomponent.getFrontier()); + } + } + return list; + } + + public static void main(String[] args) + throws SchemaLocationException, IOException { + File file = new File(args[0]); + FileType ftype = FileType.valueOf(file); + + if (ftype == FileType.PAGE) { + Page page = new PAGEPage(file); + System.out.println(""); + for (TextComponent component : page.getComponents()) { + System.out.println(component); + } + System.out.println(""); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/SortPageXML.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/SortPageXML.java new file mode 100644 index 00000000..26cdf712 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/SortPageXML.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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, transform to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.xml.DocumentBuilder; +import eu.digitisation.xml.DocumentParser; +import eu.digitisation.xml.DocumentWriter; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * PAGE-XML regions order in the document can differ form reading order. Such + * information is stored under OrdereGroup (recursive) elements. This class + * restores the appropriate order of elements in the document. + * + * @author R.C.C. + */ +public class SortPageXML { + + /** + * SortPageXML children consistently with the order defined for their id + * attribute Remark: NodeList mutates when operations on nodes take place: + * an backup childList is used to avoid such conflicts. + * + * @param node the parent node + * @param order the array of id's in ascending order + */ + private static void sort(Node node, List order) { + Map index = new HashMap(); // index of children nodes + NodeList children = node.getChildNodes(); + List childList = new ArrayList(); // External copy of children + + // Initialize index (only nodes which need reordering will be stored) + for (String id : order) { + index.put(id, null); + } + + // Create an index ot text regions which need reordering + for (int n = 0; n < children.getLength(); ++n) { + Node child = children.item(n); + childList.add(child); + if (child instanceof Element) { + String id = ((Element) child).getAttribute("id"); + if (index.containsKey(id)) { + index.put(id, child); + } + } + } + + // Clear internal list of child nodes + while (children.getLength() > 0) { + node.removeChild(children.item(0)); + } + + int norder = 0; // the posititon in the order list + for (int n = 0; n < childList.size(); ++n) { + Node child = childList.get(n); + if (child instanceof Element) { + String id = ((Element) child).getAttribute("id"); + + if (index.containsKey(id)) { + Node replacement = index.get(order.get(norder)); + node.appendChild(replacement); + ++norder; + } else { + node.appendChild(child); + } + } else { + node.appendChild(child); + } + } + } + + /** + * Extract reading order as defined by an OrderedGroup element + * + * @param e the OrderedGroup element + * @return list of identifiers in reading order + * @throws IOException + */ + private static List readingOrder(Node node) throws IOException { + NodeList children = node.getChildNodes(); + List order = new ArrayList(); + + for (int n = 0; n < children.getLength(); ++n) { + Node child = children.item(n); + if (child instanceof Element + && child.getNodeName().equals("RegionRefIndexed")) { + String index = ((Element) child).getAttribute("index"); + assert Integer.parseInt(index) == order.size(); + String idref = ((Element) child).getAttribute("regionRef"); + order.add(idref); + } + } + return order; + } + + /** + * + * @param doc a PAGE XML document + * @return true if doc is transformed according to the reading order + * @throws IOException + */ + public static boolean isSorted(Document doc) throws IOException { + NodeList groups = doc.getElementsByTagName("OrderedGroup"); + for (int n = 0; n < groups.getLength(); ++n) { + Node group = groups.item(n); + List order = readingOrder(group); + Node container = group.getParentNode().getParentNode(); + NodeList children = container.getChildNodes(); + + int nreg = 0; // region number in group + for (int k = 0; k < children.getLength(); ++k) { + Node child = children.item(k); + if (child instanceof Element) { + String tag = child.getNodeName(); + if (tag.equals("TextRegion")) { + String id = ((Element) child).getAttribute("id"); + if (order.contains(id) && // sometimes item is not listed + !id.equals(order.get(nreg++))) { + return false; + } + } + } + } + } + return true; + } + + /** + * + * @param file a PAGE XML input file + * @return true if the document in file is transformed according to the + * reading order + * @throws IOException + */ + public static boolean isSorted(File file) throws IOException { + Document doc = DocumentParser.parse(file); + return isSorted(doc); + } + + /** + * Create document where ordered groups follow the order information + * + * @param source the input document to be transformed + * @return the transformed document + * @throws java.io.IOException + */ + public static Document sorted(Document source) throws IOException { + Document doc = DocumentBuilder.clone(source); + NodeList groups = doc.getElementsByTagName("OrderedGroup"); + for (int n = 0; n < groups.getLength(); ++n) { + Node group = groups.item(n); + // group element -> ReadingOrder-> OrderedGroup + Node container = group.getParentNode().getParentNode(); + List order = readingOrder(group); + sort(container, order); + } + return doc; + } + + /** + * Create a file where ordered groups follow the order information + * + * @param ifile the input PAGE XML file + * @param ofile the file with the transformed document + * @throws java.io.IOException + */ + public static void transform(File ifile, File ofile) throws IOException { + Document doc = DocumentParser.parse(ifile); + DocumentWriter writer = new DocumentWriter(SortPageXML.sorted(doc)); + writer.write(ofile); + } + + /** + * Demo main + * + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + File ifile = new File(args[0]); + + System.out.println(SortPageXML.isSorted(ifile)); + if (args.length > 1) { + File ofile = new File(args[1]); + SortPageXML.transform(ifile, ofile); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/TextComponent.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/TextComponent.java new file mode 100644 index 00000000..ec8b7e05 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/TextComponent.java @@ -0,0 +1,121 @@ +package eu.digitisation.layout; + +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +import java.awt.Polygon; + +/** + * A region in a document (a page, a block, line, or word) + * + * @author R.C.C. + */ +public class TextComponent { + + private static final long serialVersionUID = 1L; + + String id; // identifier + ComponentType type; // the type of component (page, block, line, word) + String subtype; // the type of block (paragraph, header, TOC). + String content; // text content + Polygon frontier; // the component frontier + + /** + * Basic constructor + * + * @param id identifier + * @param type the type of component (page, block, line, word) + * @param subtype the type of block (paragraph, header, TOC). + * @param content text content + * @param frontier the component frontier + */ + public TextComponent(String id, ComponentType type, String subtype, + String content, Polygon frontier) { + this.id = id; + this.type = type; + this.subtype = subtype; + this.content = content; + this.frontier = frontier; + } + + /** + * + * @return the component identifier + */ + public String getId() { + return id; + } + + /** + * + * @return the type of this component + */ + public ComponentType getType() { + return type; + } + + /** + * + * @return the subtype of this component + */ + public String getSubtype() { + return subtype; + } + + /** + * Get the text content + * + * @return the textual contend of this component + */ + public String getContent() { + return content; + } + + /** + * Get the frontier + * + * @return the polygonal frontier of this component + */ + public Polygon getFrontier() { + return frontier; + } + + /** + * + * @return a string representation of this TextComponent + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("\n") + .append("\t").append(id).append("\n") + .append("\t").append(type).append("\n") + .append("\t").append(subtype).append("\n") + .append("\t").append(content).append("\n") + .append("\t\n"); + if (frontier != null) { + for (int n = 0; n < frontier.npoints; ++n) { + builder.append("\t\t").append(frontier.xpoints[n]).append("") + .append("").append(frontier.ypoints[n]).append("\n"); + } + } + builder.append("\t\n"); + builder.append("\n"); + return builder.toString(); + } + } diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/layout/Viewer.java b/ocrevalUAtion/src/main/java/eu/digitisation/layout/Viewer.java new file mode 100644 index 00000000..45aaea3d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/layout/Viewer.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import eu.digitisation.image.Bimage; +import eu.digitisation.input.FileType; +import eu.digitisation.input.SchemaLocationException; +import java.awt.Color; +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; + +/** + * Shows text regions (as stored in PAGE XML) on image + * + * @author R.C.C + */ +public class Viewer { + + /** + * Split a file name into path, base-name and extension + * + * @param filename + * @return path (before last separator), base-name (before last dot) and + * extension (after last dot) + */ + private static String[] getFilenameTokens(String filename) { + String[] tokens = new String[3]; + int first = filename.lastIndexOf(File.separator); + int last = filename.lastIndexOf('.'); + tokens[0] = filename.substring(0, first); + tokens[1] = filename.substring(first + 1, last); + tokens[2] = filename.substring(last + 1); + return tokens; + } + + /** + * Demo main + * + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException, SchemaLocationException { + if (args.length < 2) { + System.err.println("Usage: Viewer image_file page_file [options]"); + System.exit(0); + } + + File ifile = new File(args[0]); + File xmlfile = new File(args[1]); + String opts = (args.length > 2) ? args[2] : null; + FileType ftype = FileType.valueOf(xmlfile); + String[] tokens = getFilenameTokens(args[0]); + String path = tokens[0]; + String id = tokens[1]; + String ext = tokens[2]; + File ofile = new File(path + File.separator + id + "_marked." + ext); + + Bimage page = null; + Bimage scaled; + float[] shortDash = {4f, 2f}; + float[] longDash = {8f, 4f}; + + Page gt = null; + + if (ifile.exists()) { + try { + page = new Bimage(ifile).toRGB(); + } catch (NullPointerException ex) { + throw new IOException("Unsupported format"); + } + } else { + throw new java.io.IOException(ifile.getCanonicalPath() + " not found"); + } + if (xmlfile.exists()) { + switch (ftype) { + case PAGE: + gt = new PAGEPage(xmlfile); + break; + case HOCR: + gt = new HOCRPage(xmlfile); + break; + case FR10: + gt = new FR10Page(xmlfile); + break; + case ALTO: + gt = new ALTOPage(xmlfile); + break; + default: + throw new java.lang.UnsupportedOperationException("Still not implemented"); + } + } else { + throw new java.io.IOException(xmlfile.getCanonicalPath() + " not found"); + } + + if (opts == null || opts.contains("b")) { + page.add(gt.getFrontiers(ComponentType.BLOCK), Color.RED, 8f); + } + if (opts == null || opts.contains("l")) { + page.add(gt.getFrontiers(ComponentType.LINE), Color.GREEN, 2f, longDash); + } + if (opts == null || opts.contains("w")) { + page.add(gt.getFrontiers(ComponentType.WORD), Color.BLUE, 2f); + } + + + for (TextComponent component : gt.getComponents(ComponentType.WORD)) { + System.out.println(component); + // page.add(component.getFrontier(), Color.BLUE, 2f); + } + + scaled = new Bimage(page, 1.0); + scaled.write(ofile); + System.out.println("output=" + ofile); + + if (opts != null && opts.contains("s")) { + if (Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + Desktop.getDesktop().open(ofile); + } + } + + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFormatter.java b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFormatter.java new file mode 100644 index 00000000..d8a663f5 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFormatter.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.log; + +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * + * @author R.C.C. + */ +public class LogFormatter extends Formatter { + + public LogFormatter() { + super(); + } + + @Override + public String format(LogRecord record) { + StringBuilder builder = new StringBuilder(); + + builder.append(record.getLevel().getName()); + builder.append(" "); + builder.append(formatMessage(record)); + builder.append("\n"); + + return builder.toString(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrame.java b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrame.java new file mode 100644 index 00000000..19e418e7 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrame.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.log; + +import java.awt.Container; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +/** + * + * @author R.C.C. + */ +public class LogFrame extends JFrame { + + private static final long serialVersionUID = 1L; + private final Container pane; + private final JTextArea text; + + public LogFrame() { + this.setTitle("Operations log"); + setSize(600, 300); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + text = new JTextArea(); + pane = getContentPane(); + pane.add(new JScrollPane(text)); + setVisible(true); + + } + + public void showInfo(String data) { + text.append(data); + //this.validate(); + //this.repaint(); + } + + public void close() { + setVisible(false); + dispose(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrameHandler.java b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrameHandler.java new file mode 100644 index 00000000..8633bf64 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/log/LogFrameHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.log; + +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * + * @author R.C.C. + */ +public class LogFrameHandler extends Handler { + + private LogFrame frame = new LogFrame(); + + @Override + public void publish(LogRecord record) { + if (frame == null) { + frame = new LogFrame(); + } + if (!frame.isVisible()) { + frame.setVisible(true); + } + if (isLoggable(record)) { + String message = getFormatter().format(record); + frame.showInfo(message); + } + } + + @Override + public void flush() { + frame.revalidate(); + frame.repaint(); + } + + @Override + public void close() { + frame = null; + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/log/Messages.java b/ocrevalUAtion/src/main/java/eu/digitisation/log/Messages.java new file mode 100644 index 00000000..32e59516 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/log/Messages.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.log; + +import java.io.File; + +/** + * + * @author R.C.C. + */ +public class Messages { + + // private static final Logger logger = Logger.getLogger("ApplicationLog"); + + static { + // try { + // URI uri = Messages.class.getProtectionDomain() + // .getCodeSource().getLocation().toURI(); + // String dir = new File(uri.getPath()).getParent(); + // File file = new File(dir, "ocrevaluation.log"); + // + // addFile(file); + // Messages.info("Logfile is " + file); + // + // // while debugging + // if (java.awt.Desktop.isDesktopSupported()) { + // addFrame(); + // } + // } catch (SecurityException ex) { + // Messages.info(Messages.class.getName() + ": " + ex); + // } catch (URISyntaxException ex) { + // Logger.getLogger(Messages.class.getName()).log(Level.SEVERE, null, + // ex); + // } + } + + public static void addFile(File file) { + // try { + // FileHandler fh = new FileHandler(file.getAbsolutePath()); + // fh.setFormatter(new LogFormatter()); + // logger.addHandler(fh); + // } catch (IOException ex) { + // Messages.info(Messages.class.getName() + ": " + ex); + // } + } + + public static void addFrame() { + // Only while debugging + // LogFrameHandler lfh = new LogFrameHandler(); + // lfh.setFormatter(new LogFormatter()); + // logger.addHandler(lfh); + } + + public static void info(String s) { + // logger.info(s); + } + + public static void warning(String s) { + // logger.warning(s); + } + + public static void severe(String s) { + // logger.severe(s); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/Arrays.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/Arrays.java new file mode 100644 index 00000000..6f552a2a --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/Arrays.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2013 R.C.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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +/** + * Standard operations on arrays: sum, average, max, min, standard deviation. + * + * @author R.C.C. + * @version 20131110 + */ +public class Arrays { + + /** + * @param array array of int + * @return the sum of all ints in array + */ + public static int sum(int[] array) { + int sum = 0; + + for (int n = 0; n < array.length; ++n) { + sum += array[n]; + } + return sum; + } + + /** + * + * @param array array of double + * @return the sum of all doubles in array + */ + public static double sum(double[] array) { + double sum = 0; + for (int n = 0; n < array.length; ++n) { + sum += array[n]; + } + return sum; + } + + /** + * Transform an int array into a an array of doubles + * + * @param array integer array + * @return the array of double precision values + */ + public static double[] toDouble(int[] array) { + double[] darray = new double[array.length]; + for (int i = 0; i < array.length; i++) { + darray[i] = array[i]; + } + return darray; + } + + /** + * Create an array containing the logarithms of the source array + * + * @param array integer array + * @return the array of logs + */ + public static double[] log(int[] array) { + double[] darray = new double[array.length]; + for (int i = 0; i < array.length; i++) { + darray[i] = Math.log(array[i]); + } + return darray; + } + + /** + * Create an array containing the logarithms of the source array + * + * @param array array of double + * @return the array of logs + */ + public static double[] log(double[] array) { + double[] darray = new double[array.length]; + for (int i = 0; i < array.length; i++) { + darray[i] = Math.log(array[i]); + } + return darray; + } + + /** + * @param array + * @return the average of all integers in an array + */ + public static double average(int[] array) { + return sum(array) / (double) array.length; + } + + /** + * The average of all doubles in an array + * + * @param array + * @return the average of all doubles in an array + */ + public static double average(double[] array) { + return sum(array) / array.length; + } + + /** + * @param array + * @return the geometric mean (log-average) of all integers in an array + */ + public static double logaverage(int[] array) { + return Math.exp(average(log(array))); + } + + /** + * The geometric mean (log-average) of all doubles in an array + * + * @param array + * @return the average of all doubles in an array + */ + public static double logaverage(double[] array) { + return Math.exp(average(log(array))); + } + + /** + * The scalar product + * + * @param x the first array + * @param y the second array + * @return the scalar product of x and y + */ + public static double scalar(double[] x, double[] y) { + double sum = 0; + for (int n = 0; n < x.length; ++n) { + sum += x[n] * y[n]; + } + return sum; + } + + /** + * @param array int array + * @return the max value in int array + */ + public static int max(int[] array) { + int mu = array[0]; + + for (int n = 1; n < array.length; ++n) { + mu = Math.max(mu, array[n]); + } + return mu; + } + + /** + * @param array array of doubles + * @return the max value in this array + */ + public static double max(double[] array) { + double mu = array[0]; + + for (int n = 1; n < array.length; ++n) { + mu = Math.max(mu, array[n]); + } + return mu; + } + + /** + * @param array int array + * @return the min value in int array + */ + public static int min(int[] array) { + int mu = array[0]; + + for (int n = 1; n < array.length; ++n) { + mu = Math.min(mu, array[n]); + } + return mu; + } + + /** + * @param array array of doubles + * @return the min value in this array + */ + public static double min(double[] array) { + double mu = array[0]; + + for (int n = 1; n < array.length; ++n) { + mu = Math.min(mu, array[n]); + } + return mu; + } + + /** + * @param array int array + * @return first position containing the max value in int array + */ + public static int argmax(int[] array) { + int pos = 0; + + for (int n = 1; n < array.length; ++n) { + if (array[n] > array[pos]) { + pos = n; + } + } + return pos; + } + + /** + * @param array int array + * @return first position containing the min value in int array + */ + public static int argmin(int[] array) { + int pos = 0; + + for (int n = 1; n < array.length; ++n) { + if (array[n] < array[pos]) { + pos = n; + } + } + return pos; + } + + /** + * @param X array of int + * @param Y another array of int + * @return the covariance of two variables X and Y are expected to have same + * length + */ + public static double cov(int[] X, int[] Y) { + int len = Math.min(X.length, Y.length); + double sum = 0; // double safer against overflows + + for (int n = 0; n < len; ++n) { + sum += X[n] * (double) Y[n]; + } + + return sum / len - average(X) * average(Y); + } + + /** + * Covariance of two variables + * + * @param X array of double + * @param Y another array of double + * @return Covariance of X and Y + */ + public static double cov(double[] X, double[] Y) { + int len = Math.min(X.length, Y.length); + double sum = 0; + + for (int n = 0; n < len; ++n) { + sum += X[n] * Y[n]; + } + return sum / len - average(X) * average(Y); + } + + /** + * @param X the array with the values of the variable + * @return the standard deviation of the values in X + */ + public static double std(int[] X) { + return Math.sqrt(cov(X, X)); + } + + /** + * Standard deviation + * + * @param X the array with the values of the variable + * @return the standard deviation of the values in X + */ + public static double std(double[] X) { + return Math.sqrt(cov(X, X)); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/BiCounter.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/BiCounter.java new file mode 100644 index 00000000..cdd2be2f --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/BiCounter.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import java.util.Map; +import java.util.Set; + +/** + * Keeps a counter for pairs of objects (joint frequencies) and marginal counts + * + * @author R.C.C. + * @param the type of first object + * @param the type of the second object + */ +public class BiCounter, T2 extends Comparable> + extends Counter> { + + private static final long serialVersionUID = 1L; + Counter subtotal1; + Counter subtotal2; + + /** + * Default constructor + */ + public BiCounter() { + super(); + subtotal1 = new Counter(); + subtotal2 = new Counter(); + } + + /** + * + * @param o1 first component in pair to be incremented + * @param o2 second component in pair to be incremented + * @param value the value to be added for the pair count + * @return this BiCounter + */ + public BiCounter add(T1 o1, T2 o2, int value) { + Pair pair = new Pair(o1, o2); + super.add(pair, value); + subtotal1.add(o1, value); + subtotal2.add(o2, value); + return this; + } + + /** + * Set the value for a count and reset accordingly the total and marginal + * counts + * + * @param o1 first component in pair + * @param o2 second component in pair + * @param value the value for the pair count + * @return this BiCounter + */ + public BiCounter set(T1 o1, T2 o2, int value) { + Pair pair = new Pair(o1, o2); + super.set(pair, value); + subtotal1.set(o1, value); + subtotal2.set(o2, value); + return this; + } + + /** + * Add one to the count for a pair + * + * @param o1 first component in pair to be incremented + * @param o2 second component in pair to be incremented + * @return this BiCounter + */ + public BiCounter inc(T1 o1, T2 o2) { + return add(o1, o2, 1); + } + + /** + * Subtract one to the count for a pair + * + * @param o1 first component in the pair to be decremented + * @param o2 second component in the pair be decremented + * @return this BiCounter + */ + public BiCounter dec(T1 o1, T2 o2) { + return add(o1, o2, -1); + } + + /** + * Increment the count for an pair with the value stored in another + * BiCounter. + * + * @param counter the counter whose values will be added to this one. + * @return this BiCounter + */ + public BiCounter add(BiCounter counter) { + for (Map.Entry, Integer> entry : counter.entrySet()) { + Pair key = entry.getKey(); + Integer value = entry.getValue(); + add(key.first, key.second, value); + } + return this; + } + + /** + * + * @param o1 first component in pair + * @param o2 second component in pair + * @return the value of the counter for that pair, or 0 if not stored. If + * one the components is null the marginal count is returned. + * @throws NullPointerException if both objects are null + */ + public int value(T1 o1, T2 o2) { + if (o1 == null) { + return subtotal2.value(o2); + } else if (o2 == null) { + return subtotal1.value(o1); + } else { + Pair pair = new Pair(o1, o2); + return super.value(pair); + } + } + + /** + * + * @return the set of left components in pairs of the key set + */ + public Set leftKeySet() { + return subtotal1.keySet(); + } + + /** + * + * @return the marginal counts for the left ley + */ + public Counter leftSubtotal() { + return subtotal1; + } + + /** + * + * @return the set of right components in pairs of the key set + */ + public Set rightKeySet() { + return subtotal2.keySet(); + } + + /** + * + * @return the marginal counts for the right ley + */ + public Counter rightSubtotal() { + return subtotal2; + } + + /** + * Clear the BiCounter + */ + @Override + public void clear() { + super.clear(); + subtotal1.clear(); + subtotal2.clear(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/Counter.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/Counter.java new file mode 100644 index 00000000..7d3d4b43 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/Counter.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Counts number of different objects, a map between objects and integers which + * can be incremented and decremented. + * + * @version 2012.06.07 + * @param the class of objects being counted + */ +public class Counter> + extends java.util.TreeMap { + + private static final long serialVersionUID = 1L; + + int total = 0; // stores aggregated counts + + /** + * Increment the count for an object with the given value + * + * @param object the object whose count will be incremented + * @param value the delta value + * @return this Counter + */ + public Counter add(Type object, int value) { + int storedValue; + if (containsKey(object)) { + storedValue = get(object); + } else { + storedValue = 0; + } + put(object, storedValue + value); + total += value; + return this; + } + + /** + * Set the count for an object (and the global one) with the given value + * + * @param object the object whose count will be incremented + * @param value the value for this count + * @return this Counter + */ + public Counter set(Type object, int value) { + int storedValue; + if (containsKey(object)) { + storedValue = get(object); + } else { + storedValue = 0; + } + put(object, value); + total += value - storedValue; + return this; + } + + /** + * Add one to the count for an object + * + * @param object the object whose count will be incremented + * @return this Counter + */ + public Counter inc(Type object) { + return add(object, 1); + } + + /** + * Subtract one to the count for an object + * + * @param object the object whose count will be decremented + * @return this Counter + */ + public Counter dec(Type object) { + return add(object, -1); + } + + /** + * Increment the count for an object with the value stored in another + * counter. + * + * @param counter the counter whose values will be added to this one. + * @return this Counter + */ + public Counter add(Counter counter) { + for (Map.Entry entry : counter.entrySet()) { + add(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Increment counts for a an array of objects + * + * @param objects the array of objects + * @return this Counter + */ + public Counter add(Type[] objects) { + for (Type object : objects) { + inc(object); + } + return this; + } + + /** + * + * @param object + * @return the value of the counter for that object, or 0 if not stored + */ + public int value(Type object) { + Integer val = super.get(object); + return (val == null) ? 0 : val; + } + + /** + * Maximum value stored + * + * @return max stored value or 0 if no value is stored + */ + public int maxValue() { + if (size() > 0) { + return java.util.Collections.max(values()); + } else { + return 0; + } + } + + /** + * + * @return the aggregated count for all objects + */ + public int total() { + return total; + } + + /** + * Clear the counter + */ + @Override + public void clear() { + super.clear(); + total = 0; + } + + /** + * Specifies several orders for keys + */ + public enum Order { + + ASCENDING, DESCENDING, ASCENDING_VALUE, DESCENDING_VALUE, LEXICOGRAPHIC; + } + + private class KeyComparator implements Comparator { + + @Override + public int compare(Type first, Type second) { + int r = get(first).compareTo(get(second)); + return r; + } + } + + /** + * + * @param order determines ascending or descending order + * @return the sorted list of keys stored in the counter + */ + public List keyList(Order order) { + List list = new ArrayList(keySet()); + if (order == Order.ASCENDING) { + Collections.sort(list); + } else if (order == Order.DESCENDING) { + Collections.sort(list, Collections.reverseOrder()); + } else if (order == Order.ASCENDING_VALUE) { + Collections.sort(list, new KeyComparator()); + } else if (order == Order.DESCENDING_VALUE) { + Collections.sort(list, Collections.reverseOrder(new KeyComparator())); + } else if (order == Order.LEXICOGRAPHIC + && list.size() > 0 + && list.get(0).getClass().equals(String.class)) { + Collator collator = Collator.getInstance(); + collator.setStrength(Collator.TERTIARY); + collator.setDecomposition(Collator.FULL_DECOMPOSITION); + Collections.sort(list, collator); + } + return list; + } + + /** + * A list key-value pairs + * + * @return a string containing one key and value per line + */ + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + for (Type key : keySet()) { + b.append(key).append(" ").append(get(key)).append("\n"); + } + return b.toString(); + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/Histogram.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/Histogram.java new file mode 100644 index 00000000..d1b88f6a --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/Histogram.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * Creates toy histograms + * + * @author R.C.C. + */ +public class Histogram { + + private static final long serialVersionUID = 1L; + String title; + int[] X; + int[] Y; + + /** + * Create points for plot (one point per bar, equally spaced). + * + * @param + * @param title the title for this histogram + * @param counter a Counter + */ + public > Histogram(String title, Counter counter) { + int n = 0; + + for (Type key : counter.keySet()) { + Object obj = key; + if (obj instanceof Integer) { + Integer iobj = (Integer) obj; + X[n] = iobj.intValue(); + } else { + X[n] = n; + } + Y[n] = counter.get(key); + ++n; + } + } + + /** + * Create points for plot (one point per bar, equally spaced). + * + * @param title the title for this histogram + * @param Y an array of integer values + */ + public Histogram(String title, int[] Y) { + this.title = title; + this.Y = Y; + X = new int[Y.length]; + for (int n = 0; n < Y.length; ++n) { + X[n] = n; + } + } + + /** + * Integer exponentiation (for axis) + * + * @param base base + * @param exp exponent + * @return the exp-th power of base + */ + private int pow(int base, int exp) { + int result = 1; + while (exp != 0) { + if ((exp & 1) == 1) { + result *= base; + } + exp >>= 1; + base *= base; + } + return result; + } + + /** + * Display histogram on screen + * + * @param width display width (in pixels) + * @param height display height (in pixels) + * @param margin display margins (in pixels) + */ + public void show(int width, int height, int margin) { + BufferedImage bim + = new BufferedImage(width + 2 * margin, + height + 2 * margin, + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bim.createGraphics(); + int xhigh = Arrays.max(X); + int xlow = Arrays.min(X); + int xrange = xhigh - xlow; + int yhigh = Arrays.max(Y); + int ylow = 0; //ArrayMath.min(Y); + int yrange = yhigh - ylow; + + // draw bars + g.setColor(Color.RED); + for (int n = 0; n < X.length; ++n) { + int xpos = (width * (X[n] - xlow)) / xrange; + int ypos = (height * (Y[n] - ylow)) / yrange; + g.fillRect(margin + xpos - 1, height + margin - ypos, 3, ypos); + } + + // draw title + g.setColor(Color.DARK_GRAY); + if (title != null) { + g.drawString(title, margin, margin / 2); + } + + // draw X and Y axes + g.setColor(Color.BLUE); + g.drawRect(margin, margin, width, height); + + // draw Y-tics + int e = (int) Math.ceil(Math.log(yrange) / Math.log(10)) - 1; + int ystep = (e > 0) ? pow(10, e) : 1; + for (int y = ylow - ylow % ystep; y <= yhigh; y += ystep) { + int ypos = (height * (y - ylow)) / yrange; + g.drawString(String.valueOf(y) + "-", 0, height + margin - ypos); + } + + // draw X-tics + e = (int) Math.ceil(Math.log(xrange) / Math.log(10)) - 1; + int xstep = (e > 0) ? pow(10, e) : 1; + for (int x = xlow - xlow % xstep; x <= xhigh; x += xstep) { + int xpos = (width * (x - xlow)) / xrange; + g.drawString(String.valueOf(x), margin + xpos - 6 * e, height + margin + 12); + g.drawLine(margin + xpos, height + margin, + margin + xpos, height + margin - 5); + } + + g.dispose(); + eu.digitisation.image.Display.draw(bim); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/MinimalPerfectHash.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/MinimalPerfectHash.java new file mode 100644 index 00000000..f16c9efc --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/MinimalPerfectHash.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import eu.digitisation.log.Messages; +import eu.digitisation.text.WordScanner; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +/** + * Mapping between strings and integers A MinimalPerfectHash guarantees + * consistency between TokenArrays since the mapping between words and integer + * codes is shared by all TokenArrays created by the same factory and this + * allows for the comparison of TokenArrays. + * + * @version 2013.12.10 + */ +public class MinimalPerfectHash { + + /** + * The codes assigned to strings (tokens) + */ + private final HashMap codes; // token->code mapping + private final List dictionary; // code->token mapping + boolean caseSensitive; // Case sensitive encoding + + /** + * Create a new MinimalPerfectHash + * + * @param caseSensitive true if the TokenArrays must be case sensitive + */ + public MinimalPerfectHash(boolean caseSensitive) { + codes = new HashMap(); + dictionary = new ArrayList(); + this.caseSensitive = caseSensitive; + } + + /** + * Default constructor (case sensitive factory) + */ + public MinimalPerfectHash() { + this(true); + } + + /** + * + * @param word a word + * @return the integer code assigned to this word + */ + private Integer hashCode(String word) { + Integer code; + String key = caseSensitive ? word : word.toLowerCase(); + + if (codes.containsKey(key)) { + code = codes.get(key); + } else { + code = codes.size(); + codes.put(key, code); + dictionary.add(key); + } + return code; + } + + /** + * + * @param code an integer code + * @return the string or word associated with this code + */ + public String decode(int code) { + return dictionary.get(code); + } + + /** + * Build an array of hash codes from the file content + * + * @param file the input file + * @param encoding the text encoding. + * @return the list of hash codes representing the file content + */ + public List hashCodes(File file, Charset encoding) + throws RuntimeException { + ArrayList list = new ArrayList(); + + try { + WordScanner scanner = new WordScanner(file, encoding, null); + String word; + + while ((word = scanner.nextWord()) != null) { + list.add(hashCode(word)); + } + } catch (IOException ex) { + Messages.info(MinimalPerfectHash.class.getName() + ": " + ex); + } + return list; + } + + /** + * Build a TokenArray from a String + * + * @param s the input string + * @return the list of hash codes representing the file content + */ + public List hashCodes(String s) { + ArrayList list = new ArrayList(); + + try { + WordScanner scanner = new WordScanner(s, null); + String word; + + while ((word = scanner.nextWord()) != null) { + list.add(hashCode(word)); + } + } catch (IOException ex) { + Messages.info(MinimalPerfectHash.class.getName() + ": " + ex); + } + + return list; + } + + /** + * + * @return the list of all strings with a hash code in this map + */ + public List keys() { + return dictionary; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/Pair.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/Pair.java new file mode 100644 index 00000000..d3caa41a --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/Pair.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +/** + * A pair of objects (not necessarily of identical type) + * + * @param the type of first object + * @param the type of second object + */ +public class Pair, T2 extends Comparable> + implements Comparable> { + + public T1 first; // first element in pair + public T2 second; // second element in pair + + /** + * Default class constructor + */ + public Pair() { + } + + /** + * Class constructor + * @param first first component + * @param second second component + */ + public Pair(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + /** + * Comparator + * @param other another pair + */ + @Override + public int compareTo(Pair other) { + if (this.first.equals(other.first)) { + return this.second.compareTo(other.second); + } else { + return this.first.compareTo(other.first); + } + + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object o) { + Pair other; + if (o == null) { + return false; + } else { + if (o instanceof Pair) { + other = (Pair) o; + } else { + throw new ClassCastException(Pair.class + + " cannot be compared with " + + o.getClass()); + } + return this.first.equals(other.first) && this.second.equals(other.second); + } + } + + @Override + public int hashCode() { + return first.hashCode() ^ 31 * second.hashCode(); + } + + @Override + public String toString() { + return "(" + first.toString() + "," + second.toString() + ")"; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/math/Plot.java b/ocrevalUAtion/src/main/java/eu/digitisation/math/Plot.java new file mode 100644 index 00000000..9f918567 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/math/Plot.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * Create a dummy plot for a function + * + * @author R.C.C. + */ +public class Plot { + + double[] X; + double[] Y; + + public Plot(double[] X, double[] Y) { + this.X = X; + this.Y = Y; + } + + /** + * Integer exponentiation (for axis) + * + * @param base base + * @param exp exponent + * @return the exp-th power of base + */ + private int pow(int base, int exp) { + int result = 1; + while (exp != 0) { + if ((exp & 1) == 1) { + result *= base; + } + exp >>= 1; + base *= base; + } + return result; + } + + /** + * Display histogram on screen + * + * @param width display width (in pixels) + * @param height display height (in pixels) + * @param margin display margins (in pixels) + */ + public void show(int width, int height, int margin) { + BufferedImage bim + = new BufferedImage(width + 2 * margin, + height + 2 * margin, + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bim.createGraphics(); + double xhigh = Arrays.max(X); + double xlow = Arrays.min(X); + double xrange = xhigh - xlow; + double yhigh = Arrays.max(Y); + double ylow = 0; //ArrayMath.min(Y); + double yrange = yhigh - ylow; + + // draw bars + g.setColor(Color.RED); + for (int n = 0; n < X.length; ++n) { + int xpos = (int)((width * (X[n] - xlow)) / xrange); + int ypos = (int)((height * (Y[n] - ylow)) / yrange); + g.fillRect(margin + xpos - 1, height + margin - ypos, 3, ypos); + } + + // draw X and Y axes + g.setColor(Color.BLUE); + g.drawRect(margin, margin, width, height); + + // draw Y-tics + int e = (int) Math.ceil(Math.log(yrange) / Math.log(10)) - 1; + int ystep = (e > 0) ? pow(10, e) : 1; + for (int y = ystep * (int)(ylow/ystep); y <= yhigh; y += ystep) { + int ypos = (int)((height * (y - ylow)) / yrange); + g.drawString(String.valueOf(y) + "-", 0, height + margin - ypos); + } + + // draw X-tics + e = (int) Math.ceil(Math.log(xrange) / Math.log(10)) - 1; + int xstep = (e > 0) ? pow(10, e) : 1; + for (int x = xstep * (int)(xlow/xstep); x <= xhigh; x += xstep) { + int xpos = (int)((width * (x - xlow)) / xrange); + g.drawString(String.valueOf(x) , margin + xpos - 6 * e, height + margin + 12); + g.drawLine(margin + xpos, height + margin, + margin + xpos, height + margin - 5); + } + + g.dispose(); + System.out.println("Showing on screen"); + eu.digitisation.image.Display.draw(bim); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/ContextLengthRange.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/ContextLengthRange.java new file mode 100644 index 00000000..48cdbbbb --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/ContextLengthRange.java @@ -0,0 +1,45 @@ +package eu.digitisation.ngram; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ContextLengthRange { + private int start; + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + private int end; + + public ContextLengthRange(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Creates ContextLengthRange objects based on the provided string + * + * @param contextLengthRange + * context length range string which needs to follow the + * following regex: [1-9]-[1-9] where the first digit is a start + * and the second is end + * @return ContextLengthRange object created from the given string + */ + public static ContextLengthRange parseContextLengthRange( + String contextLengthRange) { + Pattern clPattern = Pattern.compile("([1-9])-([1-9])"); + Matcher clMatcher = clPattern.matcher(contextLengthRange); + if (clMatcher.find()) { + return new ContextLengthRange(Integer.parseInt(clMatcher.group(1)), + Integer.parseInt(clMatcher.group(2))); + } else { + throw new IllegalArgumentException( + "Context length needs to be in format of [1-9]-[1-9]!"); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Distance.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Distance.java new file mode 100644 index 00000000..a10ec36d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Distance.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +/** + * Several types of distances between n-gram models + * + * @author R.C.C. + */ +public class Distance { + + /** + * + * @param first an NgramModel + * @param second a second NgramModel (its order must be identical to + * first's) + * @return + */ + public static double[] delta(NgramModel first, NgramModel second) { + double[] result = new double[first.order]; + int[] deltas = new int[first.order]; + int[] tot = new int[first.order]; + + if (first.order != second.order) { + throw new IllegalArgumentException("Illegal comparison " + + "of n-gram models with different n"); + } + for (String s : first.occur.keySet()) { + if (s.length() > 0) { + int val1 = first.occur.get(s).getValue(); + int val2 = second.occur.containsKey(s) + ? second.occur.get(s).getValue() : 0; + deltas[s.length() - 1] += Math.abs(val1 - val2); + tot[s.length() - 1] += (val1 + val2); + } + } + for (String s : second.occur.keySet()) { + if (s.length() > 0 && !first.occur.containsKey(s)) { + int val2 = second.occur.get(s).getValue(); + deltas[s.length() - 1] += val2; + tot[s.length() - 1] += val2; + } + } + for (int n = 0; n < first.order; ++n) { + //result += Math.log(deltas[n] / (double) tot[n]); + result[n] = deltas[n] / (double) tot[n]; + } +// return Math.exp(result / first.order); + return result; + } + + /** + * + * @param first an NgramModel + * @param second a second NgramModel (its order must be identical to + * first's) + * @return + */ + public static double cosine(NgramModel first, NgramModel second) { + int sum = 0; + int norm1 = 0; + int norm2 = 0; + + if (first.order != second.order) { + throw new IllegalArgumentException("Illegal comparison " + + "of n-gram models with different n"); + } + for (String s : first.occur.keySet()) { + if (s.length() > 0) { + int val1 = first.occur.get(s).getValue(); + int val2 = second.occur.containsKey(s) + ? second.occur.get(s).getValue() : 0; + sum += val1 * val2; + norm1 += val1 * val1; + norm2 += val2 * val2; + } + } + for (String s : second.occur.keySet()) { + if (s.length() > 0 && !first.occur.containsKey(s)) { + int val2 = second.occur.get(s).getValue(); + norm2 += val2 * val2; + } + } + if (norm1 > 0 && norm2 > 0) { + return sum / (Math.sqrt(norm1) * Math.sqrt(norm2)); + } else { + return 0; + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/EvaluationFrame.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/EvaluationFrame.java new file mode 100644 index 00000000..8e8b6360 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/EvaluationFrame.java @@ -0,0 +1,243 @@ +package eu.digitisation.ngram; + +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Font; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +public class EvaluationFrame extends JFrame { + + /** + * + */ + private static final long serialVersionUID = 4895806099667768081L; + + private JPanel contentPane; + private JTextField thresholdTextField; + private JSlider thresholdSlider; + private JTextPane textPane; + + /** + * double array containing the perplexity values for every character. + */ + private double[] perplexityArray; + + /** + * text style applied when the threshold value is exceeded. + */ + private static final SimpleAttributeSet thresholdExceededStyle = new SimpleAttributeSet(); + /** + * default text style. + */ + private static final SimpleAttributeSet defaultStyle = new SimpleAttributeSet(); + + static { + StyleConstants.setForeground(thresholdExceededStyle, Color.red); + StyleConstants.setForeground(defaultStyle, Color.black); + } + + /** + * Create the frame. + */ + public EvaluationFrame() { + init(); + } + + /** + * GUI initialization. + */ + private void init() { + setBounds(100, 100, 473, 347); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + setContentPane(contentPane); + + JPanel panel = new JPanel(); + + JScrollPane scrollPane = new JScrollPane(); + GroupLayout gl_contentPane = new GroupLayout(contentPane); + gl_contentPane.setHorizontalGroup(gl_contentPane.createParallelGroup(Alignment.LEADING) + .addComponent(panel, GroupLayout.DEFAULT_SIZE, 447, Short.MAX_VALUE) + .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 447, Short.MAX_VALUE)); + gl_contentPane.setVerticalGroup(gl_contentPane.createParallelGroup(Alignment.LEADING).addGroup( + gl_contentPane + .createSequentialGroup() + .addComponent(panel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE).addPreferredGap(ComponentPlacement.RELATED) + .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE))); + + textPane = new JTextPane(); + textPane.setFont(new Font("Tahoma", Font.PLAIN, 16)); + scrollPane.setViewportView(textPane); + + thresholdTextField = new JTextField(); + thresholdTextField.setEditable(false); + thresholdTextField.setFont(new Font("Tahoma", Font.BOLD, 16)); + thresholdTextField.setText("-1"); + thresholdTextField.setColumns(10); + + thresholdSlider = new JSlider(); + thresholdSlider.setValue(-1); + thresholdSlider.setMaximum(-1); + thresholdSlider.setMinimum(-50); + thresholdSlider.addChangeListener(new ChangeListener() { + + @Override + public void stateChanged(ChangeEvent e) { + if (!thresholdSlider.getValueIsAdjusting()) { + thresholdTextField.setText((double) thresholdSlider.getValue() + ""); + update(true); + } + } + }); + GroupLayout gl_panel = new GroupLayout(panel); + gl_panel.setHorizontalGroup(gl_panel.createParallelGroup(Alignment.TRAILING).addGroup( + gl_panel.createSequentialGroup() + .addComponent(thresholdSlider, GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(thresholdTextField, GroupLayout.PREFERRED_SIZE, 74, GroupLayout.PREFERRED_SIZE) + .addContainerGap())); + gl_panel.setVerticalGroup(gl_panel.createParallelGroup(Alignment.TRAILING).addGroup( + gl_panel.createSequentialGroup() + .addContainerGap() + .addGroup( + gl_panel.createParallelGroup(Alignment.TRAILING) + .addComponent(thresholdSlider, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 31, + Short.MAX_VALUE) + .addComponent(thresholdTextField, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, + 31, Short.MAX_VALUE)).addContainerGap())); + panel.setLayout(gl_panel); + contentPane.setLayout(gl_contentPane); + + } + + /** + * update the evaluation results. + */ + private void update(boolean thresholdMode) { + Double threshold = 0.0; + try { + threshold = Double.parseDouble(thresholdTextField.getText()); + StyledDocument document = textPane.getStyledDocument(); + + document.setCharacterAttributes(0, document.getLength(), defaultStyle, true); + if (thresholdMode) { + for (int i = 0; i < perplexityArray.length; i++) { + if (perplexityArray[i] < threshold) { + document.setCharacterAttributes(i, 1, thresholdExceededStyle, true); + } + } + } else { + double max = 0; + + for (int i = 0; i < perplexityArray.length; i++) { + double value = perplexityArray[i]; + if (!Double.isInfinite(value)) { + if (Math.abs(value) > Math.abs(max)) { + max = value; + } + } + } + + List colors = getColorBands(Color.red, 11); + + for (int i = 0; i < perplexityArray.length; i++) { + SimpleAttributeSet style = new SimpleAttributeSet(); + double value = perplexityArray[i]; + if (Double.isInfinite(value)) { + StyleConstants.setForeground(style, Color.red); + } else { + int color = 10 - (int) ((value / max) * 10); + StyleConstants.setForeground(style, colors.get(color)); + } + document.setCharacterAttributes(i, 1, style, true); + } + } + + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(this, "Unable to parse value '" + + thresholdTextField.getText() + "'", + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + public void setInput(String textToEvaluate, double[] perplexityArray) { + this.perplexityArray = perplexityArray; + textPane.setText(textToEvaluate); + update(false); + } + + public List getColorBands(Color color, int bands) { + + List colorBands = new ArrayList(bands); + for (int index = 0; index < bands; index++) { + colorBands.add(darken(color, index / (double) bands)); + } + return colorBands; + + } + + public static Color darken(Color color, double fraction) { + + int red = (int) Math.round(Math.max(0, color.getRed() - 255 * fraction)); + int green = (int) Math.round(Math.max(0, color.getGreen() - 255 * fraction)); + int blue = (int) Math.round(Math.max(0, color.getBlue() - 255 * fraction)); + + int alpha = color.getAlpha(); + + return new Color(red, green, blue, alpha); + + } + + /** + * Launch the application. + * + * @param args + */ + public static void main(final String[] args) { + + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + File model = new File(args[0]); + File input = new File(args[1]); + int contextLenght = Integer.parseInt(args[2]); + + TextPerplexity result + = new TextPerplexity(new NgramModel(model), + new FileInputStream(input), contextLenght); + + EvaluationFrame frame = new EvaluationFrame(); + frame.setInput(result.getText(), result.getPerplexities()); + frame.setVisible(true); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + }); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Experiment.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Experiment.java new file mode 100644 index 00000000..d19c7821 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Experiment.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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, transform to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import eu.digitisation.input.WarningException; +import eu.digitisation.layout.SortPageXML; +import eu.digitisation.output.ErrorMeasure; +import eu.digitisation.text.CharFilter; +import eu.digitisation.text.Text; +import java.io.File; +import java.io.IOException; + +/** + * + * @author R.C.C. + */ +public class Experiment { + + private static void compare(File f1, File f2, File f3, CharFilter filter) + throws WarningException, IOException { + Text c1 = new Text(f1); + Text c2 = new Text(f2); + Text c3 = new Text(f3); + String s1 = c1.toString(filter); + String s2 = c2.toString(filter); + String s3 = c3.toString(filter); + boolean sorted = SortPageXML.isSorted(f1); + int l1 = s1.length(); + final int N = 10; + + if (l1 < 100) { + System.err.println("Text is too short (" + l1 + " characters)"); + } else { + double cer12 = ErrorMeasure.cer(s1, s2); + double cer32 = ErrorMeasure.cer(s3, s2); + double[] errors; + double cosineDist; + + NgramModel m1 = new NgramModel(N); + NgramModel m2 = new NgramModel(N); + m1.addWord(s1); + m2.addWord(s2); + + cosineDist = 1 - Distance.cosine(m1, m2); + errors = Distance.delta(m1, m2); + + System.out.print(f1.getName() + " " + sorted + + " " + String.format("%05d", s1.length()) + + " " + String.format("%.3f", cer12) + + " " + String.format("%.3f", cer32) + + " " + String.format("%.3f", cosineDist)); + for (int n = 1; n <= N; ++n) { + System.out.print(" " + String.format("%.3f", errors[n - 1])); + } + System.out.println(); + } + } + + public static void main(String[] args) + throws IOException, WarningException { + File dir1 = new File(args[0]); + File dir2 = new File(args[1]); + File dir3 = new File("/tmp"); + CharFilter filter = args.length > 2 + ? new CharFilter(new File(args[2])) + : new CharFilter(true); + if (dir1.isFile()) { + dir3 = new File("/tmp", + dir1.getName().replace(".xml", "_sorted.xml")); + + if (dir2.exists()) { + SortPageXML.transform(dir1, dir3); + Experiment.compare(dir1, dir2, dir3, filter); + } + } else { + for (File f1 : dir1.listFiles()) { + String name = f1.getName(); + File f2 = new File(dir2, name.replace(".xml", ".html")); + File f3 = new File(dir3, name.replace(".xml", "_sorted.xml")); + if (f2.exists()) { + SortPageXML.transform(f1, f3); + compare(f1, f2, f3, filter); + } + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Int.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Int.java new file mode 100644 index 00000000..2b68329b --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/Int.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import java.io.Serializable; + +/** + * A mutable Integer (faster than boxing/un-boxing) + * + * @author Rafael C. Carrasco + * @version 1.1 + */ +public class Int implements Comparable, Serializable { + private static final long serialVersionUID = 1L; + + int n; // The value. + + /** + * Constructs a new Int with the specified int value. + * + * @param n the integer value + */ + public Int(int n) { + this.n = n; + } + + /** + * Constructs a new Int with the value indicated by the string. + * + * @param s string representing the integer value + */ + public Int(String s) { + n = Integer.parseInt(s); + } + + /** + * Returns the value as int. + * + * @return he value as int + */ + public int getValue() { + return n; + } + + /** + * Assigns the specified int value. + * + * @param n the value for this Int + * @return the Int itself + */ + public Int setValue(int n) { + this.n = n; + return this; + } + + /** + * Pre-increments value by 1. + * + * @return the Int itself + */ + public Int increment() { + ++n; + return this; + } + + /** + * Add to value. + * + * @param n the delta value + * @return he Int itself + */ + public Int add(int n) { + this.n += n; + return this; + } + + /** + * Add to value. + * + * @param n the delta value + * @return he Int itself + */ + public Int subtact(int n) { + this.n -= n; + return this; + } + + /** + * Pre-decrements value by 1. + * + * @return he Int itself + */ + public Int decrement() { + --n; + return this; + } + + /** + * Post-increments value by one. + * + * @return he Int itself + */ + public Int postIncValue() { + ++n; + return new Int(n - 1); + } + + /** + * Returns a new Int with incremented value. + * + * @return he Int itself + */ + public Int nextInt() { + return new Int(n + 1); + } + + /** + * Returns a String object representing the specified Int. + */ + @Override + public String toString() { + return String.valueOf(n); + } + + /** + * Tests if two Int objects store the same value. + * @param other another Int object + * @return true if values are identical + */ + public boolean equals(Int other) { + return this.n == other.n; + } + + /** + * Tests if this Int objects stores a given value. + * @param n an integer value + * @return true if his Int objects stores n + */ + public boolean equals(int n) { + return this.n == n; + } + + /** + * Compares this object to the specified object. The result is true if and + * only if the argument is not null and is an Int object that contains the + * same int value as this object. + * @param object another pair + */ + @Override + public boolean equals(Object object) { + if (object == null) { + return false; + } else if (this == object) { + return true; + } + if (object instanceof Int) { + return this.n == ((Int) object).n; + } else { + return false; + } + + } + + @Override + public int hashCode() { + return n; + } + + /** + * Compares two Int objects numerically. + * @param N another Int object + */ + @Override + public int compareTo(Int N) { + if (n < N.n) { + return -1; + } else { + return (n == N.n) ? 0 : 1; + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.tmp b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.tmp new file mode 100644 index 00000000..7e809f14 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.tmp @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import eu.digitisation.log.Messages; +import eu.digitisation.text.WordScanner; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * A n-gram model for strings. N is the maximal order of the model (context + * length plus one). + */ +public class NgramModel implements Serializable { + + static final long serialVersionUID = 1L; + static final String BOS = "\u0002"; // Begin of string text marker. + static final String EOS = "\u0003"; // End of text marker. + int order; // The size of the context plus one (n-gram). + HashMap occur; // Number of occurrences. + double[] lambda; // Backoff parameters + + /** + * Set maximal order of model. + * + * @param order the size of the context plus one (the n in n-gram). + */ + public final void setOrder(int order) { + if (occur == null || occur.isEmpty()) { + if (order > 0) { + this.order = order; + } else { + throw new IllegalArgumentException("Order must be grater than 0"); + } + } else { + throw new IllegalStateException("Cannot change order of model with previous content"); + } + } + + /** + * Class constructor (default order is 2). + */ + public NgramModel() { + setOrder(2); + occur = new HashMap(); + lambda = null; + + } + + /** + * Class constructor. + * + * @param order the size of the context plus one. + */ + public NgramModel(int order) { + setOrder(order); + occur = new HashMap(); + lambda = null; + } + + /** + * @return number of different n-grams stored + */ + public int size() { + return occur.keySet().size(); + } + + /** + * Save n-gram model to GZIP file + * + * @param file the output file + */ + public void save(File file) { + try { + FileOutputStream fos = new FileOutputStream(file); + GZIPOutputStream gos = new GZIPOutputStream(fos); + ObjectOutputStream out = new ObjectOutputStream(gos); + out.writeObject(this); + out.close(); + } catch (IOException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * Build n-gram model from file + * + * @param file the GZIP input file + */ + public NgramModel(File file) { + try { + FileInputStream fis = new FileInputStream(file); + GZIPInputStream gis = new GZIPInputStream(fis); + ObjectInputStream in = new ObjectInputStream(gis); + NgramModel ngram = (NgramModel) in.readObject(); + in.close(); + this.order = ngram.order; + this.occur = ngram.occur; + this.lambda = ngram.lambda; + } catch (IOException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } catch (ClassNotFoundException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * @return Good-Turing back-off parameters. + */ + public double[] getGoodTuringPars() { + double[] pars = new double[order]; + int[] total = new int[order]; + int[] singles = new int[order]; + for (String word : occur.keySet()) { + if (word.length() > 0) { + int k = word.length() - 1; + int times = occur.get(word).getValue(); + total[k] += times; + if (times == 1) { + ++singles[k]; + } + } + } + for (int k = 0; k < order; ++k) { + pars[k] = singles[k] / (double) total[k]; + } + return pars; + } + + /** + * @param n n-gram order. + * @return Good-Turing back-off parameter. + */ + private double lambda(int n) { + if (lambda == null) { + lambda = getGoodTuringPars(); + } + return lambda[n]; + } + + /** + * @param s a k-gram + * @return the (k-1)-gram obtained by removing its first character. + */ + private String tail(String s) { + return s.substring(1); + } + + /** + * @param s a k-gram + * @return the (k-1)-gram obtained by removing its last character. + */ + private String head(String s) { + return s.substring(0, s.length() - 1); + } + + /** + * @return the number of text entries (usually words) building the model. + */ + private int numWords() { + return occur.get(EOS).getValue(); // end-of-word. + } + + /** + * @param s a k-gram (k > 0) + * @return the conditional probability of the k-gram, normalized to the + * number of heads. + */ + public double prob(String s) { + if (occur.containsKey(s)) { + String h = head(s); + if (h.endsWith(BOS)) { // since head is not stored + return occur.get(s).getValue() / (double) numWords(); + } else { + return occur.get(s).getValue() + / (double) occur.get(h).getValue(); + } + } else { + return 0; + } + } + + /** + * @param s a k-gram + * @return the conditional probability of the k-gram, normalized to the + * frequency of its heads and interpolated with lower order models. + */ + public double smoothProb(String s) { + double result; + if (s.length() > 1) { + double lam = lambda(s.length() - 1); + result = (1 - lam) * prob(s) + lam * smoothProb(tail(s)); + } else { + result = prob(s); + } + return result; + } + + /** + * @param s a k-gram + * @return the expected number of occurrences (per word) of s. + */ + private double expectedNumberOf(String s) { + if (s.endsWith(BOS)) { + return 1; + } else { + return occur.get(s).getValue() / (double) numWords(); + } + } + + /** + * Increments number of occurrences of s. + * + * @param s a k-gram. + */ + private void addEntry(String s) { + if (occur.containsKey(s)) { + occur.get(s).increment(); + } else { + occur.put(s, new Int(1)); + } + } + + /** + * Increments number of occurrences of s. + * + * @param s a k-gram. + * @param n number of occurrences + */ + private void addEntries(String s, int n) { + if (occur.containsKey(s)) { + occur.get(s).add(n); + } else { + occur.put(s, new Int(n)); + } + } + + /** + * Compute n-gram model log entropy per word (in bits). + * + * @return log entropy per word (in bits). + */ + public double entropy() { + double p, sum = 0; + for (String s : occur.keySet()) { + if (s.length() == order) { + p = prob(s); + sum -= expectedNumberOf(head(s)) * p * Math.log(p); + } + } + return sum / Math.log(2); + } + + /** + * Extracts all k-grams in a word or text upto the maximal order. For + * instance, if word = "ma" and order = 3, then 0-grams are: "" (three empty + * strings, used to normalize 1-grams); three uni-grams: "m, a, $" ($ + * represents end-of-string); three bi-grams: "#m, ma, a$" (# is used to + * differentiate #m from 1-gram m); and three tri-grams: "##m, #ma, ma$" + * + * @remark never add uni-gram "#" to the model because the normalization of + * uni-grams will be wrong! + * @param word the word or text (string of characters) to be added. + */ + public void add(String word) { + if (word.length() < 1) { + throw new IllegalStateException("Cannot extract n-grams from empty word"); + } else { + word += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < word.length(); ++last) { + s = tail(s) + word.charAt(last); + for (int first = 0; first <= s.length(); ++first) { + addEntry(s.substring(first)); + } + } + } + + /** + * Add all k-grams in a word or text + * + * @param word the word or text to be processed + * @param times the number of occurrences of the word or text + */ + public void add(String word, int times) { + if (word.length() < 1) { + throw new IllegalStateException("Cannot extract n-grams from empty word"); + } else { + word += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < word.length(); ++last) { + s = tail(s) + word.charAt(last); + for (int first = 0; first <= s.length(); ++first) { + addEntries(s.substring(first), times); + } + } + } + + /** + * Reads text file and adds words to model. + * + * @param file a text file + * @param encoding the text encoding + * @param caseSensitive true if extracted n-grams are case sensitive + */ + public void addWords(File file, Charset encoding, boolean caseSensitive) { + try { + WordScanner scanner = new WordScanner(file, encoding, "^\\p{Space}+"); + String word; + while ((word = scanner.nextWord()) != null) { + if (caseSensitive) { + add(word); + } else { + add(word.toLowerCase()); + + } + } + } catch (IOException ex) { + Messages.info(NgramModel.class + .getName() + ": " + ex); + } + } + + /** + * Compute probability of a word or text + * + * @param text a word or a sequence of characters + * @return the log-probability (base e) of the sequence + */ + public double logProb(String text) { + double res = 0; + if (text.length() < 1) { + throw new IllegalArgumentException("Cannot compute probability of empty word"); + } else { + text += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < text.length(); ++last) { + s = tail(s) + text.charAt(last); + + double p = smoothProb(s); + if (p == 0) { + System.err.println(s + " has 0 probability"); + return Double.NEGATIVE_INFINITY; + } else { + res += Math.log(p); + } + } + return res; + } + + /** + * Compute probability of a character after a given context. This + * implementation only takes into account the preceding context + * + * @param context a sequence of characters + * @param c a character + * @return the log-probability (base e) that the character c follows the + * given context + */ + public double logProb(String context, char c) { + double res = 0; + int len = context.length() + 1; // the length of the context + character + String s = len > order + ? context.substring(len - order) + c + : context + c; + double p = smoothProb(s); + + return (p > 0) + ? Math.log(p) + : Double.NEGATIVE_INFINITY; + } + + /** + * Reads input text and computes per-word cross entropy. + * + * @param caseSensitive true if the model is case sensitive + * @return the log-likelihood of text 8per word). + */ + public double logPerWordLikelihood(boolean caseSensitive) { + try { + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + WordScanner scanner = new WordScanner(System.in, encoding); + String word; + double result = 0; + int numWords = 0; + while ((word = scanner.nextWord()) != null) { + ++numWords; + if (caseSensitive) { + result -= logProb(word); + } else { + result -= logProb(word.toLowerCase()); + } + } + + return result / numWords / Math.log(2); + + } catch (IOException ex) { + Messages.info(NgramModel.class + .getName() + ": " + ex); + } + return Double.POSITIVE_INFINITY; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (String s : occur.keySet()) { + builder.append(s.replaceAll(BOS, "").replaceAll(EOS, "")); + builder.append(' ').append(occur.get(s)).append('\n'); + } + return builder.toString(); + } + + /** + * Show differences between model (debug function) + * + * @param other another NgramModel (order must coincide) + */ + public void showDiff(NgramModel other) { + if (this.order != other.order) { + throw new IllegalArgumentException("Illegal comparison " + + "of n-gram models with different n"); + } + for (String s : this.occur.keySet()) { + if (s.length() > 0) { + int val1 = this.occur.get(s).getValue(); + int val2 = other.occur.containsKey(s) + ? other.occur.get(s).getValue() : 0; + if (val1 != val2) { + System.out.println(s + " " + val1 + " " + val2); + } + } + } + for (String s : other.occur.keySet()) { + if (s.length() > 0 && !this.occur.containsKey(s)) { + int val2 = other.occur.get(s).getValue(); + System.out.println(s + " 0 " + val2); + } + } + } + + /** + * Main function. + * + * @param args + */ + public static void main(String[] args) { + NgramModel ngram = new NgramModel(); + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + File fout = null; + + if (args.length == 0) { + System.err.println("Usage: Ngram [-n n] [-e encoding] [-o outfile]" + + " file1 file2 ...."); + } else { + for (int k = 0; k < args.length; ++k) { + String arg = args[k]; + + if (arg.equals("-n")) { + ngram.setOrder(new Integer(args[++k])); + } else if (arg.equals("-e")) { + encoding = Charset.forName(args[++k]); + } else if (arg.equals("-o")) { + fout = new File(args[++k]); + } else { + ngram.addWords(new File(arg), encoding, false); + } + } + if (fout != null) { + ngram.save(fout); + } else { + System.out.println(ngram.entropy()); + System.out.println(ngram.logPerWordLikelihood(false)); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.txt b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.txt new file mode 100644 index 00000000..01bdcf38 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/MNgramModel.txt @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import eu.digitisation.log.Messages; +import eu.digitisation.text.WordScanner; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * A n-gram model for strings. N is the maximal order of the model (context + * length plus one). + */ +public class NgramModel implements Serializable { + + static final long serialVersionUID = 1L; + static final String BOS = "\u0002"; // Begin of string text marker. + static final String EOS = "\u0003"; // End of text marker. + int order; // The size of the context plus one (n-gram). + HashMap occur; // Number of occurrences. + double[] lambda; // Backoff parameters + + /** + * Set maximal order of model. + * + * @param order the size of the context plus one (the n in n-gram). + */ + public final void setOrder(int order) { + if (occur == null || occur.isEmpty()) { + if (order > 0) { + this.order = order; + } else { + throw new IllegalArgumentException("Order must be grater than 0"); + } + } else { + throw new IllegalStateException("Cannot change order of model with previous content"); + } + } + + /** + * Class constructor (default order is 2). + */ + public NgramModel() { + setOrder(2); + occur = new HashMap(); + lambda = null; + + } + + /** + * Class constructor. + * + * @param order the size of the context plus one. + */ + public NgramModel(int order) { + setOrder(order); + occur = new HashMap(); + lambda = null; + } + + /** + * @return number of different n-grams stored + */ + public int size() { + return occur.keySet().size(); + } + + /** + * Save n-gram model to GZIP file + * + * @param file the output file + */ + public void save(File file) { + try { + FileOutputStream fos = new FileOutputStream(file); + GZIPOutputStream gos = new GZIPOutputStream(fos); + ObjectOutputStream out = new ObjectOutputStream(gos); + out.writeObject(this); + out.close(); + } catch (IOException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * Build n-gram model from file + * + * @param file the GZIP input file + */ + public NgramModel(File file) { + try { + FileInputStream fis = new FileInputStream(file); + GZIPInputStream gis = new GZIPInputStream(fis); + ObjectInputStream in = new ObjectInputStream(gis); + NgramModel ngram = (NgramModel) in.readObject(); + in.close(); + this.order = ngram.order; + this.occur = ngram.occur; + this.lambda = ngram.lambda; + } catch (IOException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } catch (ClassNotFoundException ex) { + Messages.info(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * @return Good-Turing back-off parameters. + */ + public double[] getGoodTuringPars() { + double[] pars = new double[order]; + int[] total = new int[order]; + int[] singles = new int[order]; + for (String word : occur.keySet()) { + if (word.length() > 0) { + int k = word.length() - 1; + int times = occur.get(word).getValue(); + total[k] += times; + if (times == 1) { + ++singles[k]; + } + } + } + for (int k = 0; k < order; ++k) { + pars[k] = singles[k] / (double) total[k]; + } + return pars; + } + + /** + * @param n n-gram order. + * @return Good-Turing back-off parameter. + */ + private double lambda(int n) { + if (lambda == null) { + lambda = getGoodTuringPars(); + } + return lambda[n]; + } + + /** + * @param s a k-gram + * @return the (k-1)-gram obtained by removing its first character. + */ + private String tail(String s) { + return s.substring(1); + } + + /** + * @param s a k-gram + * @return the (k-1)-gram obtained by removing its last character. + */ + private String head(String s) { + return s.substring(0, s.length() - 1); + } + + /** + * @return the number of text entries (usually words) building the model. + */ + private int numWords() { + return occur.get(EOS).getValue(); // end-of-word. + } + + /** + * @param s a k-gram (k > 0) + * @return the conditional probability of the k-gram, normalized to the + * number of heads. + */ + public double prob(String s) { + if (occur.containsKey(s)) { + String h = head(s); + if (h.endsWith(BOS)) { // since head is not stored + return occur.get(s).getValue() / (double) numWords(); + } else { + return occur.get(s).getValue() + / (double) occur.get(h).getValue(); + } + } else { + return 0; + } + } + + /** + * @param s a k-gram + * @return the conditional probability of the k-gram, normalized to the + * frequency of its heads and interpolated with lower order models. + */ + public double smoothProb(String s) { + double result; + if (s.length() > 1) { + double lam = lambda(s.length() - 1); + result = (1 - lam) * prob(s) + lam * smoothProb(tail(s)); + } else { + result = prob(s); + } + return result; + } + + /** + * @param s a k-gram + * @return the expected number of occurrences (per word) of s. + */ + private double expectedNumberOf(String s) { + if (s.endsWith(BOS)) { + return 1; + } else { + return occur.get(s).getValue() / (double) numWords(); + } + } + + /** + * Increments number of occurrences of s. + * + * @param s a k-gram. + */ + private void addEntry(String s) { + if (occur.containsKey(s)) { + occur.get(s).increment(); + } else { + occur.put(s, new Int(1)); + } + } + + /** + * Increments number of occurrences of s. + * + * @param s a k-gram. + * @param n number of occurrences + */ + private void addEntries(String s, int n) { + if (occur.containsKey(s)) { + occur.get(s).add(n); + } else { + occur.put(s, new Int(n)); + } + } + + /** + * Compute n-gram model log entropy per word (in bits). + * + * @return log entropy per word (in bits). + */ + public double entropy() { + double p, sum = 0; + for (String s : occur.keySet()) { + if (s.length() == order) { + p = prob(s); + sum -= expectedNumberOf(head(s)) * p * Math.log(p); + } + } + return sum / Math.log(2); + } + + /** + * Extracts all k-grams in a word or text upto the maximal order. For + * instance, if word = "ma" and order = 3, then 0-grams are: "" (three empty + * strings, used to normalize 1-grams); three uni-grams: "m, a, $" ($ + * represents end-of-string). three bi-grams: "#m, ma, a$" (# is used to + * differentiate #m from 1-gram m); and three tri-grams: "##m, #ma, ma$" + * + * @remark never add uni-gram "#" to the model because the normalization of + * uni-grams will be wrong! + * @param word the word (string of characters) to be added. + */ + public void addWord(String word) { + if (word.length() < 1) { + throw new IllegalStateException("Cannot extract n-grams from empty word"); + } else { + word += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < word.length(); ++last) { + s = tail(s) + word.charAt(last); + for (int first = 0; first <= s.length(); ++first) { + addEntry(s.substring(first)); + } + } + } + + /** + * Add all k-grams in a word or text + * + * @param word the word or text to be processed + * @param times the number of occurrences of the word or text + */ + public void addWords(String word, int times) { + if (word.length() < 1) { + throw new IllegalStateException("Cannot extract n-grams from empty word"); + } else { + word += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < word.length(); ++last) { + s = tail(s) + word.charAt(last); + for (int first = 0; first <= s.length(); ++first) { + addEntries(s.substring(first), times); + } + } + } + + /** + * Reads text file and adds words to model. + * + * @param file a text file + * @param encoding the text encoding + * @param caseSensitive true if extracted n-grams are case sensitive + */ + public void addTextFile(File file, String encoding, boolean caseSensitive) { + try { + WordScanner scanner = new WordScanner(file, encoding); + String word; + while ((word = scanner.nextWord()) != null) { + if (caseSensitive) { + addWord(word); + } else { + addWord(word.toLowerCase()); + + } + } + } catch (IOException ex) { + Messages.info(NgramModel.class + .getName() + ": " + ex); + } + + } + + /** + * Compute probability of a word. + * + * @param word a word. + * @return the log-probability (base e) of the contained n-grams. + */ + public double wordLogProb(String word) { + double res = 0; + if (word.length() < 1) { + throw new IllegalArgumentException("Cannot compute probability of empty word"); + } else { + word += EOS; + } + String s = ""; + while (s.length() < order) { + s += BOS; + } + for (int last = 0; last < word.length(); ++last) { + s = tail(s) + word.charAt(last); + + double p = smoothProb(s); + if (p == 0) { + System.err.println(s + " has 0 probability"); + return Double.NEGATIVE_INFINITY; + } else { + res += Math.log(p); + } + } + return res; + } + + /** + * Compute probability of a character after a given context. This + * implementation only takes into account the preceding context + * + * @param context a sequence of characters + * @param c a character + * @return the log-probability (base e) that the character c follows the + * given context + */ + public double logProb(String context, char c) { + double res = 0; + int len = context.length() + 1; // the length of the context + character + String s = len > order + ? context.substring(len - order) + c + : context + c; + double p = smoothProb(s); + + return (p > 0) + ? Math.log(p) + : Double.NEGATIVE_INFINITY; + } + + /** + * Reads input text and computes cross entropy. + * + * @param caseSensitive true if the model is case sensitive + * @return the log-likelihood of text. + */ + public double logLikelihood(boolean caseSensitive) { + try { + String encoding = System.getProperty("file.encoding"); + WordScanner scanner = new WordScanner(System.in, encoding); + String word; + double result = 0; + int numWords = 0; + while ((word = scanner.nextWord()) != null) { + ++numWords; + if (caseSensitive) { + result -= wordLogProb(word); + } else { + result -= wordLogProb(word.toLowerCase()); + } + } + + return result / numWords / Math.log(2); + + } catch (IOException ex) { + Messages.info(NgramModel.class + .getName() + ": " + ex); + } + return Double.POSITIVE_INFINITY; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (String s : occur.keySet()) { + builder.append(s.replaceAll(BOS, "").replaceAll(EOS, "")); + builder.append(' ').append(occur.get(s)).append('\n'); + } + return builder.toString(); + } + + /** + * Show differences between model (debug function) + * + * @param other another NgramModel (order must coincide) + */ + public void showDiff(NgramModel other) { + if (this.order != other.order) { + throw new IllegalArgumentException("Illegal comparison " + + "of n-gram models with different n"); + } + for (String s : this.occur.keySet()) { + if (s.length() > 0) { + int val1 = this.occur.get(s).getValue(); + int val2 = other.occur.containsKey(s) + ? other.occur.get(s).getValue() : 0; + if (val1 != val2) { + System.out.println(s + " " + val1 + " " + val2); + } + } + } + for (String s : other.occur.keySet()) { + if (s.length() > 0 && !this.occur.containsKey(s)) { + int val2 = other.occur.get(s).getValue(); + System.out.println(s + " 0 " + val2); + } + } + } + + /** + * Main function. + * + * @param args + */ + public static void main(String[] args) { + NgramModel ngram = new NgramModel(); + String encoding = System.getProperty("file.encoding"); + File fout = null; + + if (args.length == 0) { + System.err.println("Usage: Ngram [-n n] [-e encoding] [-o outfile]" + + " file1 file2 ...."); + } else { + for (int k = 0; k < args.length; ++k) { + String arg = args[k]; + + if (arg.equals("-n")) { + ngram.setOrder(new Integer(args[++k])); + } else if (arg.equals("-e")) { + encoding = args[++k]; + } else if (arg.equals("-o")) { + fout = new File(args[++k]); + } else { + ngram.addTextFile(new File(arg), encoding, false); + } + } + if (fout != null) { + ngram.save(fout); + } else { + System.out.println(ngram.entropy()); + System.out.println(ngram.logLikelihood(false)); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModel.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModel.java new file mode 100644 index 00000000..ee4aac8d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModel.java @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import eu.digitisation.log.Messages; +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.text.WordScanner; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * A n-gram model for strings. N is the maximal order of the model (context + * length plus one). + */ +public class NgramModel implements Serializable { + + static final long serialVersionUID = 1L; + static final char BOS = '\u0002'; // Begin of string text marker. + static final char EOS = '\u0003'; // End of text marker. + int order; // The size of the context plus one (n-gram). + HashMap occur; // Number of occurrences. + double[] lambda; // Backoff parameters + + /** + * Class constructor. + * + * @param order the size of the context plus one. + */ + public NgramModel(int order) { + if (order > 0) { + this.order = order; + } else { + throw new IllegalArgumentException("N-gram Order must be grater than 0"); + } + occur = new HashMap(); + lambda = null; + } + + /** + * @return number of different n-grams stored + */ + public int size() { + return occur.keySet().size(); + } + + /** + * Save n-gram model to GZIP file + * + * @param file the output file + */ + public void save(File file) { + try { + FileOutputStream fos = new FileOutputStream(file); + GZIPOutputStream gos = new GZIPOutputStream(fos); + ObjectOutputStream out = new ObjectOutputStream(gos); + + out.writeObject(this); + out.close(); + } catch (IOException ex) { + Messages.severe(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * Build n-gram model from file + * + * @param file the GZIP input file + */ + public NgramModel(File file) { + try { + FileInputStream fis = new FileInputStream(file); + GZIPInputStream gis = new GZIPInputStream(fis); + ObjectInputStream in = new ObjectInputStream(gis); + NgramModel ngram = (NgramModel) in.readObject(); + + this.order = ngram.order; + this.occur = ngram.occur; + this.lambda = ngram.lambda; + in.close(); + + Messages.info("Read " + order + "-gram model"); + } catch (IOException ex) { + Messages.severe(NgramModel.class.getName() + ": " + ex); + } catch (ClassNotFoundException ex) { + Messages.severe(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * @return Good-Turing back-off parameters. + */ + public double[] getGoodTuringPars() { + double[] pars = new double[order]; + int[] total = new int[order]; + int[] singles = new int[order]; + for (String word : occur.keySet()) { + if (word.length() > 0) { + int k = word.length() - 1; + int times = occur.get(word).getValue(); + total[k] += times; + if (times == 1) { + ++singles[k]; + } + } + } + for (int k = 0; k < order; ++k) { + pars[k] = singles[k] / (double) total[k]; + } + return pars; + } + + /** + * @param n n-gram order. + * @return Good-Turing back-off parameter. + */ + private double lambda(int n) { + if (lambda == null) { + lambda = getGoodTuringPars(); + } + return lambda[n]; + } + + /** + * @param s a string + * @return the substring obtained by removing its first character. + */ + private String tail(String s) { + return s.substring(1); + } + + /** + * @param s a string + * @return the substring obtained by removing its last character. + */ + private String head(String s) { + return s.substring(0, s.length() - 1); + } + + /** + * + * @param s a string + * @return the last character in the string + */ + private char lastChar(String s) { + return s.charAt(s.length() - 1); + } + + /** + * @return the number of strings in the sample used to build the model. + */ + private int sampleSize() { + return occur.get(String.valueOf(EOS)).getValue(); + } + + public int occurrences(String key) { + if (occur.containsKey(key)) { + return occur.get(key).getValue(); + } else { + return 0; + } + } + + /** + * @param s a non-empty string + * @return the conditional probability of the string relative to the + * probability of its head. + */ + protected double prob(String s) { + double result; + if (s.charAt(0) == BOS) { + result = occurrences(String.valueOf(BOS) + lastChar(s)) + / (double) sampleSize(); + } else if (!occur.containsKey(s)) { + result = 0; + } else { + result = occurrences(s) / (double) occurrences(head(s)); + } + return result; + } + + /** + * @param s a a non-empty string + * @return the conditional probability of the string relative to its head, + * and interpolated with lower-order models. + */ + protected double smoothProb(String s) { + double result; + + if (s.length() > 1) { + if (s.charAt(0) == BOS && s.length() > 2) { + result = smoothProb(s.substring(s.length() - 2, s.length())); + } else { + double lam = lambda(s.length() - 1); + result = (1 - lam) * prob(s) + lam * smoothProb(tail(s)); + } + } else { + result = prob(s); + } + return result; + } + + /** + * Increments number of occurrences of the given string. + * + * @param s a string + */ + protected void addEntry(String s) { + if (occur.containsKey(s)) { + occur.get(s).increment(); + } else { + occur.put(s, new Int(1)); + } + } + + /** + * Increments number of occurrences of s. + * + * @param s a k-gram. + * @param n number of occurrences + */ + protected void addEntries(String s, int n) { + if (occur.containsKey(s)) { + occur.get(s).add(n); + } else { + occur.put(s, new Int(n)); + } + } + + /** + * Extracts all k-grams in a word or text upto the maximal order. For + * instance, if word = "ma" and order = 3, then 0-grams are: "" (three empty + * strings, used to normalize 1-grams); three uni-grams: "m, a, $" ($ + * represents end-of-string); three bi-grams: "#m, ma, a$" (# is used to + * differentiate #m from 1-gram m); and two tri-grams: "#ma, ma$" + * + * @remark It does not add uni-grams "#" to the model since they can never + * appear in the middle of a word. Normalization of bi-grams starting with # + * will use n($) instead, since n(#)=n($) + * + * @param word the word or text (string of characters) to be added. + */ + public void addWord(String word) { + if (word.length() < 1) { + throw new IllegalArgumentException("Cannot extract n-grams from empty word"); + } else { + String input = BOS + word + EOS; + for (int high = 2; high <= input.length(); ++high) { + for (int low = Math.max(0, high - order); low < high; ++low) { + String s = input.substring(low, high); + addEntry(s); + } + } + addEntries("", word.length() + 1); + } + } + + /** + * Reads text file and adds the words in text to model. + * + * @param file a text file + * @param encoding the text encoding + * @param caseSensitive true if extracted n-grams are case sensitive + */ + public void addWords(File file, Charset encoding, boolean caseSensitive) { + try { + WordScanner scanner = new WordScanner(file, encoding, "^\\p{Space}+"); + String word; + while ((word = scanner.nextWord()) != null) { + if (caseSensitive) { + addWord(word); + } else { + addWord(word.toLowerCase()); + } + } + } catch (IOException ex) { + Messages.info(NgramModel.class + .getName() + ": " + ex); + } + } + + /** + * Compute probability of a word or text + * + * @param word a non-empty a sequence of characters + * @return the log-probability (base e) of this string + */ + public double logWordProb(String word) { + double res = 0; + + if (word.length() < 1) { + throw new IllegalArgumentException("Cannot compute probability of empty word"); + } else { + String input = BOS + word + EOS; + for (int high = 2; high <= input.length(); ++high) { + int low = Math.max(0, high - order); + String s = input.substring(low, high); + double p = smoothProb(s); + if (p == 0) { + Messages.warning(s + " has 0 probability"); + return Double.NEGATIVE_INFINITY; + } else { + res += Math.log(p); + } + } + } + return res; + } + + /** + * Reads input text from standard input and computes per-word cross entropy. + * + * @param caseSensitive true if the model is case sensitive + * @return the log-likelihood of input text (per word). + */ + public double logPerWordLikelihood(boolean caseSensitive) { + try { + Charset encoding = Charset.forName(System.getProperty("file.encoding")); + WordScanner scanner = new WordScanner(System.in, encoding); + + String word; + double result = 0; + int numWords = 0; + while ((word = scanner.nextWord()) != null) { + ++numWords; + if (caseSensitive) { + result -= logWordProb(word); + } else { + result -= logWordProb(word.toLowerCase()); + } + } + + return result / numWords / Math.log(2); + + } catch (IOException ex) { + Messages.severe(NgramModel.class + .getName() + ": " + ex); + } + return Double.POSITIVE_INFINITY; + } + + /** + * Add all the content in a text file + * + * @param is the input stream with text content + */ + public void addText(InputStream is) { + try { + BufferedReader reader + = new BufferedReader(new InputStreamReader(is)); + String context = String.valueOf(BOS); + + while (reader.ready()) { + String line = StringNormalizer.reduceWS(reader.readLine()); + if (!line.isEmpty()) { + String input + = (context.charAt(0) == BOS) + ? line + : " " + line; + addSubstrings(context, input); + if (input.length() >= order) { + context = input.substring(input.length() - order + 1); + } else { + String s = context + input; + context = s.substring(Math.max(0, s.length() - order + 1)); + } + } + } + addSubstrings(context, String.valueOf(EOS)); + } catch (FileNotFoundException ex) { + Messages.severe(NgramModel.class.getName() + ": " + ex); + } catch (IOException ex) { + Messages.severe(NgramModel.class.getName() + ": " + ex); + } + } + + /** + * Add all substrings in this string to the NgramModel + * + * @param context the preceding context, possibly empty + * @param text the non-empty input string + */ + protected void addSubstrings(String context, String text) { + if (text.length() < 1) { + throw new IllegalArgumentException("Cannot extract n-grams from empty text"); + } + String s = context + text; + // extract all substrings + for (int high = context.length() + 1; high <= s.length(); ++high) { + for (int low = Math.max(0, high - order); low < high; ++low) { + addEntry(s.substring(low, high)); + } + } + // the normalization of 1-grams + addEntries("", text.length()); + } + + /** + * Compute the log-likelihood (per character) of the text contained in a + * file + * + * @param is the InputStream containing the text + * @param contextLength the length of the context for the evaluation of the + * character probability + * @return + */ + public double logLikelihood(InputStream is, int contextLength) { + int nchar = 0; + double loglike = 0; + try { + BufferedReader reader + = new BufferedReader(new InputStreamReader(is)); + String context = String.valueOf(BOS); + + while (reader.ready()) { + String line = StringNormalizer.reduceWS(reader.readLine()); + if (!line.isEmpty()) { + String input + = (context.charAt(0) == BOS) + ? line + : " " + line; + + nchar += input.length(); + loglike += logLikelihood(context, input); + if (input.length() > contextLength) { + context = input.substring(input.length() - contextLength); + } else { + String s = context + input; + context = s.substring(Math.max(0, s.length() - contextLength)); + } + } + loglike += logLikelihood(context, String.valueOf(EOS)); + ++nchar; + } + } catch (IOException ex) { + Messages.warning(NgramModel.class.getName() + ": " + ex.getMessage()); + } + + return loglike / nchar; + } + + /** + * + * @param context the preceding context + * @param input a non-empty string + * @return the log probability of the string after the given context + */ + public double logLikelihood(String context, String input) { + int contextLength = context.length(); + double loglike = 0; + for (int pos = 0; pos < input.length(); ++pos) { + String s; + if (pos >= contextLength) { + s = input.substring(pos - contextLength, pos + 1); + } else { + s = context.substring(pos) + + input.substring(0, pos + 1); + } + loglike += Math.log(smoothProb(s)); + } + return loglike; + } + + /** + * Compute probability of a character after a given context. This + * implementation only takes into account the preceding context + * + * @param context a sequence of characters + * @param c a character + * @return the log-probability (base e) that the character c follows the + * given context + */ + public double logProb(String context, char c) { + double res = 0; + int len = context.length() + 1; // the length of the context + character + String s = (len > order) + ? context.substring(len - order) + c + : context + c; + double p = smoothProb(s); + + return (p > 0) + ? Math.log(p) + : Double.NEGATIVE_INFINITY; + } + + /** + * + * @return string representation of the NgramModel: keys and values + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (String key : occur.keySet()) { + String s = key.replaceAll(String.valueOf(BOS), "") + .replaceAll(String.valueOf(EOS), ""); + builder.append("'").append(s).append("' ") + .append(occur.get(key)).append('\n'); + } + return builder.toString(); + } + + /** + * Show differences between two NgramModels (debug function) + * + * @param other another NgramModel (order must coincide) + */ + public void showDiff(NgramModel other) { + if (this.order != other.order) { + throw new IllegalArgumentException("Illegal comparison " + + "of n-gram models with different n"); + } + for (String s : this.occur.keySet()) { + if (s.length() > 0) { + int val1 = this.occur.get(s).getValue(); + int val2 = other.occur.containsKey(s) + ? other.occur.get(s).getValue() : 0; + if (val1 != val2) { + System.out.println(s.replaceAll(String.valueOf(BOS), "") + .replaceAll(String.valueOf(EOS), "") + + " " + val1 + " " + val2); + } + } + } + for (String s : other.occur.keySet()) { + if (s.length() > 0 && !this.occur.containsKey(s)) { + int val2 = other.occur.get(s).getValue(); + System.out.println(s.replaceAll(String.valueOf(BOS), "") + .replaceAll(String.valueOf(EOS), "") + + " 0 " + val2); + } + } + } + + /** + * Compare two NgramModels + * + * @param other + * @return true if they store the same content + */ + public boolean equals(NgramModel other) { + if (this.order != other.order) { + return false; + } else { + for (String s : this.occur.keySet()) { + int val1 = this.occur.get(s).getValue(); + int val2 = other.occur.containsKey(s) + ? other.occur.get(s).getValue() : 0; + if (val1 != val2) { + return false; + } + + } + for (String s : other.occur.keySet()) { + if (!this.occur.containsKey(s)) { + int val2 = other.occur.get(s).getValue(); + if (val2 != 0) { + return false; + } + } + } + } + return true; + } + + @Override + public boolean equals(Object o) { + if (o instanceof NgramModel) { + NgramModel other = (NgramModel) o; + return equals(other); + } else { + return false; + } + } + + @Override + public int hashCode() { + return occur.hashCode(); + } + + /** + * Main function. + * + * @param args + * @throws java.io.FileNotFoundException + */ + public static void main(String[] args) throws FileNotFoundException { + NgramModel ngram = null; + File fout = null; + int order = 0; + + if (args.length == 0) { + System.err.println("Usage: Ngram [-n NgramModelOrder]" + + " [-i InputNgramModelFile | -o OutputNgramFile]" + + " file1 file2 ...."); + } else { + for (int k = 0; k < args.length; ++k) { + String arg = args[k]; + + if (arg.equals("-n")) { + order = Integer.parseInt(args[++k]); + ngram = new NgramModel(order); + } else if (arg.equals("-i")) { + File fin = new File(args[++k]); + ngram = new NgramModel(fin); + } else if (arg.equals("-o")) { + fout = new File(args[++k]); + } else if (ngram != null) { + File file = new File(arg); + InputStream is = new FileInputStream(file); + if (fout != null) { + ngram.addText(is); + } else if (order > 1) { + double res = ngram.logLikelihood(is, order - 1); + System.out.println(res); + } + } + } + if (fout != null) { + ngram.save(fout); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModelExaminer.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModelExaminer.java new file mode 100644 index 00000000..e592cd29 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramModelExaminer.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package eu.digitisation.ngram; + +import java.io.File; +import java.io.FileNotFoundException; + +/** + * + * @author R.C.C. + */ +public class NgramModelExaminer { + public static void main(String[] args) throws FileNotFoundException { + NgramModel model = new NgramModel(new File(args[0])); + + + for (int n = 1; n < args.length; ++n) { + String key = args[n]; + String head = key.substring(0, key.length() - 1); + int times = model.occurrences(key); + int total = model.occurrences(head); + System.out.println(times + " / " + total); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramPerplexityEvaluator.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramPerplexityEvaluator.java new file mode 100644 index 00000000..a4740ad0 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/NgramPerplexityEvaluator.java @@ -0,0 +1,63 @@ +package eu.digitisation.ngram; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +/** + * Perplexity evaluator based on an n-gram model + * + */ +public class NgramPerplexityEvaluator implements PerplexityEvaluator { + + NgramModel ngram; + + public NgramPerplexityEvaluator(NgramModel ngram) { + this.ngram = ngram; + } + + public NgramPerplexityEvaluator(File file) { + ngram = new NgramModel(file); + } + + /** + * Calculates perplexity for each character of a given text. + * + * @param textToEvaluate perplexity of characters contained in this text is + * calculated + * @param contextLength the length of character context that is considered + * when calculating perplexity + * @return array of perplexity values, each item in the array is a + * perplexity of corresponding character in the given text. + */ + @Override + public double[] calculatePerplexity(String textToEvaluate, int contextLength) { + int textLen = textToEvaluate.length(); + double[] logprobs = new double[textLen]; + for (int pos = 0; pos < textLen; ++pos) { + int beg = Math.max(0, pos - contextLength); + String context = textToEvaluate.substring(beg, pos); + logprobs[pos] = ngram.logProb(context, textToEvaluate.charAt(pos)); + } + return logprobs; + } + + public static void main(String[] args) throws FileNotFoundException { + NgramModel model = new NgramModel(new File(args[0])); + int contextLenght = Integer.parseInt(args[1]); + InputStream is = (args.length == 3) + ? new FileInputStream(new File(args[2])) + : System.in; + + TextPerplexity result + = new TextPerplexity(model, is, contextLenght); + + String text = result.getText(); + double[] perps = result.getPerplexities(); + + for (int n = 0; n < text.length(); ++n) { + System.out.println(text.charAt(n) + " " + perps[n]); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluator.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluator.java new file mode 100644 index 00000000..e33574ea --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluator.java @@ -0,0 +1,17 @@ +package eu.digitisation.ngram; + +/** + * Interface for perplexity evaluator. It calculates perplexity of characters in a given text. + * @author tparkola + * + */ +public interface PerplexityEvaluator { + + /** + * Calculates perplexity for each character of a given text. + * @param textToEvaluate perplexity of characters contained in this text is calculated + * @param contextLength the length of character context that is considered when calculating perplexity + * @return array of perplexity values, each item in the array is a perplexity of corresponding character in the given text. + */ + public double[] calculatePerplexity(String textToEvaluate, int contextLength); +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluatorAssesmentHelper.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluatorAssesmentHelper.java new file mode 100644 index 00000000..c5132a99 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/PerplexityEvaluatorAssesmentHelper.java @@ -0,0 +1,85 @@ +package eu.digitisation.ngram; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStreamWriter; + +public class PerplexityEvaluatorAssesmentHelper { + public static void main(String[] args) throws IOException { + // langModel, OCRFile, contextLengthRange, resultFile + File langModelFile = new File(args[0]); + File OCRFile = new File(args[1]); + File outputFile = new File(args[3]); + + String OCRText = extractString(OCRFile); + + NgramModel providedModel = new NgramModel(langModelFile); + + ContextLengthRange contextLengthRange = ContextLengthRange + .parseContextLengthRange(args[2]); + + double[][] perplexities = new double[contextLengthRange.getEnd() + - contextLengthRange.getStart() + 1][OCRText.length()]; + + PerplexityEvaluator logPerplexityEvaluator = new NgramPerplexityEvaluator( + providedModel); + + for (int i = contextLengthRange.getStart(); i <= contextLengthRange + .getEnd(); i++) { + perplexities[i - contextLengthRange.getStart()] = logPerplexityEvaluator.calculatePerplexity( + OCRText, i); + } + + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile))); + + bw.write(providedModel.order + "-gram model results:"); + bw.newLine(); + printPerplexities(contextLengthRange, OCRText, perplexities, bw); + } + + private static void printPerplexities( + ContextLengthRange contextLengthRange, String OCRText, + double[][] perplexities, BufferedWriter bw) throws IOException { +// System.out.print("Letter\t"); + bw.write("Letter\t"); + for (int i = contextLengthRange.getStart(); i <= contextLengthRange + .getEnd(); i++) { +// System.out.print("PC" + i + "\t"); + bw.write("PC" + i + "\t"); + } +// System.out.println(); + bw.newLine(); + for (int j = 0; j < OCRText.length(); j++) { +// System.out.print(OCRText.charAt(j) + "\t"); + bw.write(OCRText.charAt(j) + "\t"); + for (int i = 0; i < perplexities.length; i++) { +// System.out.print(perplexities[i][j] + "\t"); + bw.write(perplexities[i][j] + "\t"); + } +// System.out.println(); + bw.newLine(); + } + bw.close(); + } + + private static String extractString(File OCRFile) + throws FileNotFoundException, IOException { + BufferedReader reader = new BufferedReader(new FileReader(OCRFile)); + + StringBuffer OCRFileText = new StringBuffer(); + + String line = null; + while ((line = reader.readLine()) != null) { + OCRFileText.append(line + "\n"); + } + + reader.close(); + String OCRText = OCRFileText.toString(); + return OCRText; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/ngram/TextPerplexity.java b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/TextPerplexity.java new file mode 100644 index 00000000..acd6886e --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/ngram/TextPerplexity.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import eu.digitisation.log.Messages; +import static eu.digitisation.ngram.NgramModel.BOS; +import eu.digitisation.text.StringNormalizer; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author R.C.C. + */ +public class TextPerplexity { + + StringBuilder text; + List perplexities; + + public TextPerplexity(NgramModel ngram, InputStream is, int contextLength) { + text = new StringBuilder(); + perplexities = new ArrayList(); + try { + BufferedReader reader + = new BufferedReader(new InputStreamReader(is)); + String context = String.valueOf(BOS); + + while (reader.ready()) { + String line = StringNormalizer.reduceWS(reader.readLine()); + if (!line.isEmpty()) { + String input + = (context.charAt(0) == BOS) + ? line + : " " + line; + + for (int pos = 0; pos < input.length(); ++pos) { + String s; + if (pos >= contextLength) { + s = input.substring(pos - contextLength, pos + 1); + } else { + s = context.substring(pos) + + input.substring(0, pos + 1); + } + text.append(input.charAt(pos)); + perplexities.add(Math.log(ngram.smoothProb(s))); + + if (input.length() > contextLength) { + context = input.substring(input.length() - contextLength); + } else { + s = context + input; + context = s.substring(Math.max(0, s.length() - contextLength)); + } + } + + } + } + } catch (IOException ex) { + Messages.warning(NgramModel.class.getName() + ": " + ex.getMessage()); + } + + } + + public String getText() { + return text.toString(); + } + + public double[] getPerplexities() { + double[] array = new double[perplexities.size()]; + for (int n = 0; n < array.length; ++n) { + array[n] = perplexities.get(n); + } + return array; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/output/Browser.java b/ocrevalUAtion/src/main/java/eu/digitisation/output/Browser.java new file mode 100644 index 00000000..79509773 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/output/Browser.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.output; + +import eu.digitisation.log.Messages; +import java.awt.Desktop; +import java.awt.Desktop.Action; +import java.io.IOException; +import java.net.URI; + +/** + * Open a file or URL with an operating system application + * + * @author R.C.C. + */ +public class Browser { + + /** + * Open a URI + * + * @param uri the location of the file or resource + */ + public static void open(URI uri) { + System.out.println(uri); + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Action.BROWSE)) { + try { + Desktop.getDesktop().browse(uri); + } catch (IOException ex) { + Messages.info(Browser.class.getName() + ": " + ex); + } + } + } else { + try { + Runtime.getRuntime().exec( + "rundll32 url.dll,FileProtocolHandler " + uri); + } catch (IOException ex) { + Messages.info(Browser.class.getName() + ": " + ex); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/output/CharStatTable.java b/ocrevalUAtion/src/main/java/eu/digitisation/output/CharStatTable.java new file mode 100644 index 00000000..8b10e5f2 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/output/CharStatTable.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.output; + +import eu.digitisation.distance.EdOp; +import eu.digitisation.distance.StringEditDistance; +import eu.digitisation.math.BiCounter; +import eu.digitisation.xml.DocumentBuilder; +import org.w3c.dom.Element; + +/** + * Provide statistics of the differences between two texts + * + * @author R.C.C. + */ +public class CharStatTable extends BiCounter { + private static final long serialVersionUID = 1L; + + /** + * Create empty CharStatTable + */ + public CharStatTable() { + super(); + } + + /** + * Separate statistics of errors for every character + * + * @param s1 + * the reference text + * @param s2 + * the fuzzy text + */ + public CharStatTable(String s1, String s2) { + super(); + add(StringEditDistance.operations(s1, s2)); + } + + /** + * Separate statistics of errors for every character form a collection of + * texts + * + * @param array1 + * an array of reference texts + * @param array2 + * an array of fuzzy texts + */ + public CharStatTable(String[] array1, String[] array2) { + if (array1.length == array2.length) { + for (int n = 0; n < array1.length; ++n) { + add(StringEditDistance.operations(array1[n], array2[n])); + } + } else { + throw new java.lang.IllegalArgumentException( + "Arrays of different length"); + } + } + + /** + * Add statistic for a pair of strings + * + * @param s1 + * the reference text + * @param s2 + * the fuzzy text + */ + public void add(String s1, String s2) { + add(StringEditDistance.operations(s1, s2)); + } + + /** + * Separate statistics of errors for every character + * + * @return an element containing table with the statistics: one character + * per row and one edit operation per column. + */ + public Element asTable() { + DocumentBuilder builder = new DocumentBuilder("table"); + Element table = builder.root(); + Element row = builder.addElement("tr"); + + // features + table.setAttribute("border", "1"); + // header + builder.addTextElement(row, "th", "Character"); + builder.addTextElement(row, "th", "Character name"); + builder.addTextElement(row, "th", "Hex code"); + builder.addTextElement(row, "th", "Total"); + builder.addTextElement(row, "th", "Keep"); + builder.addTextElement(row, "th", "Insert"); + builder.addTextElement(row, "th", "Substitute"); + builder.addTextElement(row, "th", "Delete"); + builder.addTextElement(row, "th", "Error rate (%)"); + builder.addTextElement(row, "th", "Accuracy (%)"); + + // content + for (Character c : leftKeySet()) { + int spu = value(c, EdOp.INSERT); + int sub = value(c, EdOp.SUBSTITUTE); + int add = value(c, EdOp.DELETE); + int kep = value(c, EdOp.KEEP); + int tot = kep + sub + add; + double rate = (spu + sub + add) / (double) tot * 100; + double accuracy = (tot - (spu + sub + add)) / (double) tot * 100; + row = builder.addElement("tr"); + builder.addTextElement(row, "td", c.toString()); + builder.addTextElement(row, "td", Character.getName((int) c)); + builder.addTextElement(row, "td", Integer.toHexString(c)); + builder.addTextElement(row, "td", String.valueOf(tot)); + builder.addTextElement(row, "td", String.valueOf(kep)); + builder.addTextElement(row, "td", String.valueOf(spu)); + builder.addTextElement(row, "td", String.valueOf(sub)); + builder.addTextElement(row, "td", String.valueOf(add)); + builder.addTextElement(row, "td", String.format("%.2f", rate)); + builder.addTextElement(row, "td", String.format("%.2f", accuracy)); + } + return builder.document().getDocumentElement(); + } + + /** + * Prints separate statistics of errors for every character + * + * @param recordSeparator + * text between data records + * @param fieldSeparator + * text between data fields + * @return text with the statistics: every character separated by a record + * separator and every type of edit operation separated by field + * separator. + * + */ + public StringBuilder asCSV(String recordSeparator, String fieldSeparator) { + StringBuilder builder = new StringBuilder(); + + builder.append("Character") + .append(fieldSeparator).append("Hex code") + .append(fieldSeparator).append("Total") + .append(fieldSeparator).append("Keep") + .append(fieldSeparator).append("Insert") + .append(fieldSeparator).append("Substitute") + .append(fieldSeparator).append("Delete") + .append(fieldSeparator).append("Error rate (%)") + .append(fieldSeparator).append("Accuracy (%)"); + + for (Character c : leftKeySet()) { + int spu = value(c, EdOp.INSERT); + int sub = value(c, EdOp.SUBSTITUTE); + int add = value(c, EdOp.DELETE); + int kep = value(c, EdOp.KEEP); + int tot = kep + sub + add; + double rate = (spu + sub + add) / (double) tot * 100; + double accuracy = (tot - (spu + sub + add)) / (double) tot * 100; + builder.append(recordSeparator); + builder.append(c) + .append(fieldSeparator).append(Integer.toHexString(c)) + .append(fieldSeparator).append(tot) + .append(fieldSeparator).append(kep) + .append(fieldSeparator).append(spu) + .append(fieldSeparator).append(sub) + .append(fieldSeparator).append(add) + .append(fieldSeparator).append(String.format("%.2f", rate)) + .append(fieldSeparator).append( + String.format("%.2f", accuracy)); + } + return builder; + } + + /** + * Extract CER from character statistics + * + * @return the global CER + */ + public double cer() { + int spu = 0; + int sub = 0; + int add = 0; + int tot = 0; + + for (Character c : leftKeySet()) { + spu += value(c, EdOp.INSERT); + sub += value(c, EdOp.SUBSTITUTE); + add += value(c, EdOp.DELETE); + tot += value(c, EdOp.KEEP) + + value(c, EdOp.SUBSTITUTE) + + value(c, EdOp.DELETE); + } + + return (spu + sub + add) / (double) tot; + } + + public double accuracy() { + int spu = 0; + int sub = 0; + int add = 0; + int tot = 0; + + for (Character c : leftKeySet()) { + spu += value(c, EdOp.INSERT); + sub += value(c, EdOp.SUBSTITUTE); + add += value(c, EdOp.DELETE); + tot += value(c, EdOp.KEEP) + + value(c, EdOp.SUBSTITUTE) + + value(c, EdOp.DELETE); + } + + return (tot - (spu + sub + add)) / (double) tot; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/output/ErrorMeasure.java b/ocrevalUAtion/src/main/java/eu/digitisation/output/ErrorMeasure.java new file mode 100644 index 00000000..b0dde016 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/output/ErrorMeasure.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.output; + +import eu.digitisation.distance.ArrayEditDistance; +import eu.digitisation.distance.EditDistanceType; +import eu.digitisation.distance.StringEditDistance; +import eu.digitisation.document.TermFrequencyVector; +import eu.digitisation.document.TokenArray; +import eu.digitisation.log.Messages; +import eu.digitisation.math.MinimalPerfectHash; + +/** + * Computes character and word error rates by comparing two texts + * + * @version 2012.06.20 + */ +public class ErrorMeasure { + + /** + * Compute character error rate using Levenshtein distance + * + * @param s1 the reference text + * @param s2 fuzzy text + * @return character error rate with respect to the reference file + */ + public static double cer(String s1, String s2) { + int l1 = s1.length(); + int l2 = s2.length(); + double delta = (100.00 * Math.abs(l1 - l2)) / (l1 + l2); + + if (delta > 20) { + Messages.warning("Files differ a " + + String.format("%.2f", delta) + " % in character length"); + } + + return StringEditDistance.distance(s1, s2, EditDistanceType.LEVENSHTEIN) + / (double) l1; + } + + /** + * Compute character error rate using Damerau-Levenshtein distance + * + * @param s1 the reference text + * @param s2 fuzzy text + * @return character error rate with respect to the reference file + */ + public static double cerDL(String s1, String s2) { + int l1 = s1.length(); + int l2 = s2.length(); + double delta = (100.00 * Math.abs(l1 - l2)) / (l1 + l2); + + if (delta > 20) { + Messages.warning("Files differ a " + + String.format("%.2f", delta) + " % in character length"); + } + + return StringEditDistance.distance(s1, s2, EditDistanceType.DAMERAU_LEVENSHTEIN) + / (double) l1; + } + + /** + * Compute word error rate + * + * @param a1 array of integers + * @param a2 array of integers + * @return error rate + */ + private static double wer(TokenArray a1, TokenArray a2) { + int l1 = a1.length(); + int l2 = a2.length(); + double delta = (100.00 * Math.abs(l1 - l2)) / (l1 + l2); + + if (delta > 20) { + Messages.warning("Files differ a " + + String.format("%.2f", delta) + " % in word length"); + } + + return ArrayEditDistance.distance(a1.tokens(), a2.tokens(), + EditDistanceType.LEVENSHTEIN) / (double) l1; + } + + /** + * Compute word recall rate + * + * @param a1 first TokenArray + * @param a2 second TokenArray + * @return word recall (fraction of words in a1 also in a2) + */ + public static double wordRecall(TokenArray a1, TokenArray a2) { + int l1 = a1.length(); + int l2 = a2.length(); + double delta = (100.00 * Math.abs(l1 - l2)) / (l1 + l2); + + if (delta > 20) { + Messages.warning("Files differ a " + + String.format("%.2f", delta) + " % in word length"); + } + + int indel = ArrayEditDistance.distance(a1.tokens(), a2.tokens(), + EditDistanceType.INDEL); + return (l1 + l2 - indel) / (double) (2 * l1); + } + + /** + * Compute word error rate + * + * @param s1 reference text + * @param s2 fuzzy text + * @return word error rate with respect to first file + */ + public static double wer(String s1, String s2) { + MinimalPerfectHash mph = new MinimalPerfectHash(false); // case unsensitive + TokenArray a1 = new TokenArray(mph, s1); + TokenArray a2 = new TokenArray(mph, s2); + + return wer(a1, a2); + } + + /** + * Compute bag-of-word error rate + * + * @param s1 reference string + * @param s2 fuzzy string string + * @return the word error rate between the (unsorted) strings + */ + public static double ber(String s1, String s2) { + TermFrequencyVector tf1 = new TermFrequencyVector(s1); + TermFrequencyVector tf2 = new TermFrequencyVector(s2); + + return tf1.distance(tf2) / (double) tf1.total(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/output/OutputFileSelector.java b/ocrevalUAtion/src/main/java/eu/digitisation/output/OutputFileSelector.java new file mode 100644 index 00000000..ad8ae70b --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/output/OutputFileSelector.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.output; + +import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +/** + * File chooser with confirmation dialog to avoid accidental overwrite + * + * @author R.C.C. + */ +public class OutputFileSelector extends JFileChooser { + + private static final long serialVersionUID = 1L; + private static File dir; // default directory + + /** + * Default constructor + */ + public OutputFileSelector() { + super(); + } + + /** + * + * @param dir the default directory + * @param file the preselected selection + * @return the selected selection + */ + public File choose(File dir, File file) { + // Use last choice + if (OutputFileSelector.dir == null) { + OutputFileSelector.dir = dir; + } + setCurrentDirectory(OutputFileSelector.dir); + setSelectedFile(file); + + int returnVal = showOpenDialog(OutputFileSelector.this); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + File selection = getSelectedFile(); + OutputFileSelector.dir = selection.getParentFile(); + + if (selection != null && selection.exists()) { + int response = JOptionPane.showConfirmDialog(new JFrame().getContentPane(), + "The file " + selection.getName() + + " already exists.\n" + + "Do you want to replace the existing file?", + "Overwrite file", JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + + return (response == JOptionPane.YES_NO_OPTION) ? selection : null; + } + return selection; + } + return null; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/output/Report.java b/ocrevalUAtion/src/main/java/eu/digitisation/output/Report.java new file mode 100644 index 00000000..3dc07ca7 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/output/Report.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 IMPACT Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.output; + +import eu.digitisation.distance.Aligner; +import eu.digitisation.distance.EdOpWeight; +import eu.digitisation.distance.EditDistance; +import eu.digitisation.distance.EditSequence; +import eu.digitisation.distance.OcrOpWeight; +import eu.digitisation.document.TermFrequencyVector; +import eu.digitisation.input.Batch; +import eu.digitisation.input.Parameters; +import eu.digitisation.input.SchemaLocationException; +import eu.digitisation.input.WarningException; +import eu.digitisation.log.Messages; +import eu.digitisation.math.Pair; +import eu.digitisation.text.CharFilter; +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.text.Text; +import eu.digitisation.text.WordSet; +import eu.digitisation.xml.DocumentBuilder; + +import java.io.File; +import java.nio.file.Paths; + +import org.w3c.dom.Element; + +/** + * Create a report in HTML format + * + * @author R.C.C + */ +public class Report extends DocumentBuilder { + + Element head; + Element body; + private CharStatTable stats; + + /** + * Initial settings: create an empty HTML document + */ + private void init() { + head = addElement("head"); + body = addElement("body"); + // metadata + Element meta = addElement(head, "meta"); + meta.setAttribute("http-equiv", "content-type"); + meta.setAttribute("content", "text/html; charset=UTF-8"); + } + + /** + * Insert a table at the end of the document body + * + * @param content + * the table content + * @return the table element + */ + private Element addTable(Element parent, String[][] content) { + Element table = addElement(parent, "table"); + table.setAttribute("border", "1"); + for (String[] row : content) { + Element tabrow = addElement(table, "tr"); + for (String cell : row) { + addTextElement(tabrow, "td", cell); + } + } + return table; + } + + /** + * + * @param batch + * a batch of file pairs + * @param pars + * input parameters + * @throws eu.digitisation.input.WarningException + * @throws eu.digitisation.input.SchemaLocationException + */ + public Report(Batch batch, Parameters pars) + throws WarningException, SchemaLocationException { + super("html"); + init(); + + File swfile = pars.swfile.getValue(); + EdOpWeight w = new OcrOpWeight(pars); + stats = new CharStatTable(); + CharFilter filter; + + // optional eqfile + if (pars.compatibility.getValue() != null) { + filter = new CharFilter(pars.compatibility.getValue(), + pars.eqfile.getValue()); + } else { + filter = new CharFilter(pars.compatibility.getValue()); + } + + Element summaryTab; + int numwords = 0; // number of words in GT + int wdist = 0; // word distances + int bdist = 0; // bag-of-words distanbces + + addTextElement(body, "h2", "General results"); + summaryTab = addElement(body, "div"); + addTextElement(body, "h2", "Difference spotting"); + + for (int n = 0; n < batch.size(); ++n) { + Pair input = batch.pair(n); + Messages.info("Processing " + input.first.getName()); + Text gt = new Text(input.first); + Text ocr = new Text(input.second); + String gtref = pars.ignoreDiacritics.getValue() // remove spurious + // marks + ? gt.toString(filter).replaceAll( + " \\p{InCombiningDiacriticalMarks}+", " ") + : gt.toString(filter); + String ocrref = pars.ignoreDiacritics.getValue() + ? ocr.toString(filter) // remove spurious marks + .replaceAll(" \\p{InCombiningDiacriticalMarks}+", " ") + : ocr.toString(filter); + String gts = StringNormalizer.canonical(gtref, + pars.ignoreCase.getValue(), + pars.ignoreDiacritics.getValue(), + false); + String ocrs = StringNormalizer.canonical(ocrref, + pars.ignoreCase.getValue(), + pars.ignoreDiacritics.getValue(), + false); + EditSequence eds = new EditSequence(gts, ocrs, w, 2000); + TermFrequencyVector gtv = new TermFrequencyVector(gts); + TermFrequencyVector ocrv = new TermFrequencyVector(ocrs); + Element alitab = Aligner.bitext(input.first.getName(), + input.second.getName(), gtref, ocrref, w, eds); + int[] wd = (swfile == null) + ? EditDistance.wordDistance(gts, ocrs, 1000) + : EditDistance.wordDistance(gts, ocrs, new WordSet(swfile), + 1000); + + stats.add(eds.stats(gtref, ocrref, w)); + addTextElement(body, "div", " "); + addElement(body, alitab); + numwords += wd[0]; // length (words) in gts + wdist += wd[2]; // word-based distance + bdist += gtv.distance(ocrv); + } + // Summary table + double cer = stats.cer(); + double accuracy = stats.accuracy(); + double wer = wdist / (double) numwords; + double ber = bdist / (double) numwords; + String[][] summaryContent = { + { "CER %", String.format("%.2f", cer * 100) }, + // {"CER (with swaps)", String.format("%.2f", cerDL * 100)}, + { "WER %", String.format("%.2f", wer * 100) }, + { "WER % (order independent)", String.format("%.2f", ber * 100) }, + { "Character accuracy %", String.format("%.2f", accuracy * 100) } + }; + addTable(summaryTab, summaryContent); + // CharStatTable + addTextElement(body, "h2", "Error rate per character and type"); + addElement(body, stats.asTable()); + } + + public CharStatTable getStats() { + return stats; + } + + public static void main(String[] args) { + if (args.length != 2) { + System.err.println("Usage: aligner file1 file2"); + } else { + Element alitab = + Aligner.alignmentMap("", "", args[0], args[1], null); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/CharFilter.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/CharFilter.java new file mode 100644 index 00000000..5d3f7037 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/CharFilter.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.input.ExtensionFilter; +import eu.digitisation.log.Messages; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Map; + +/** + * Transform text according to a mapping between (source, target) Unicode + * character sequences. This can be useful, for example, to replace Unicode + * characters which are not supported by the browser or editor with printable + * ones. It also performs canonicalization an returns the recommended normal + * form (NFC = composed or NFKC if compatibility mode is selected). + * + * @version 2012.06.20 + */ +public class CharFilter extends HashMap { + + private static final long serialVersionUID = 1L; + boolean compatibility; // Unicode compatibility mode + + /** + * Default constructor + */ + public CharFilter() { + super(); + this.compatibility = false; + } + + /** + * Default constructor + * + * @param compatibility + * the Unicode compatibility mode (true means activated) + * @param file + * a CSV file with one transformation per line, each line + * contains two Unicode hex sequences (and comments) separated + * with commas + */ + public CharFilter(boolean compatibility, File file) { + super(); + this.compatibility = compatibility; + addFilter(file); + } + + /** + * Default constructor + * + * @param compatibility + * the Unicode compatibility mode (true means activated) + */ + public CharFilter(boolean compatibility) { + super(); + this.compatibility = compatibility; + } + + /** + * Constructor that inherits all entries of the given source map. + * + * @param compatibility + * @param source + */ + public CharFilter(boolean compatibility, Map source) { + super(source.size()); + this.compatibility = compatibility; + this.putAll(source); + } + + /** + * Load the transformation map from a CSV file: one transformation per line, + * each line contains two Unicode hex sequences (and comments) separated + * with commas + * + * @param file + * the CSV file (or directory with CSV files) with the equivalent + * sequences + */ + public CharFilter(File file) { + this.compatibility = false; + addFilter(file); + } + + /** + * Add files to filter + * + * @param file + * the CSV file (or directory with CSV files) with the equivalent + * sequences + */ + public final void addFilter(File file) { + if (file.isDirectory()) { + String[] filenames = file.list(new ExtensionFilter(".csv")); + for (String filename : filenames) { + addCSV(new File(filename)); + } + } else if (file.isFile()) { + addCSV(file); + } + } + + /** + * Add the equivalences contained in a CSV file + * + * @param file + * the CSV file + */ + private void addCSV(File file) { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + while (reader.ready()) { + String line = reader.readLine(); + String[] tokens = line.split("([,;\t])"); + if (tokens.length > 1) { // allow comments in line + String key = UnicodeReader.codepointsToString(tokens[0]); + String value = UnicodeReader.codepointsToString(tokens[1]); + put(key, value); + } else { + throw new IOException("Wrong line" + line + + " at file " + file); + } + } + reader.close(); + } catch (IOException ex) { + + } + } + + /** + * Add the equivalences in CSV format + * + * @param reader + * a BufferedReader with CSV lines + */ + public void addCSV(BufferedReader reader) { + try { + while (reader.ready()) { + String line = reader.readLine(); + String[] tokens = line.split("([,;\t])"); + if (tokens.length > 1) { // allow comments in line + String key = UnicodeReader.codepointsToString(tokens[0]); + String value = UnicodeReader.codepointsToString(tokens[1]); + put(key, value); + System.out.println(key + ", " + value); + } else { + throw new IOException("Wrong CSV line" + line); + } + } + reader.close(); + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + } + + /** + * Set the compatibility mode + * + * @param compatibility + * the compatibility mode + */ + public void setCompatibility(boolean compatibility) { + this.compatibility = compatibility; + } + + /** + * Find all occurrences of characters in a sequence and substitute them with + * the replacement specified by the transformation map. Remark: No + * replacement priority is guaranteed in case of overlapping matches. + * + * @param s + * the string to be transformed + * @return a new string with all the transformations performed + */ + public String translate(String s) { + String r = compatibility + ? StringNormalizer.compatible(s) + : StringNormalizer.composed(s); + for (Map.Entry entry : entrySet()) { + r = r.replaceAll(entry.getKey(), entry.getValue()); + } + return r; + } + + /** + * Converts the contents of a file into a CharSequence + * + * @param file + * the input file + * @return the file content as a CharSequence + */ + public CharSequence toCharSequence(File file) { + try { + FileInputStream input = new FileInputStream(file); + FileChannel channel = input.getChannel(); + java.nio.ByteBuffer buffer = channel.map( + FileChannel.MapMode.READ_ONLY, 0, channel.size()); + return java.nio.charset.Charset.forName("utf-8").newDecoder() + .decode(buffer); + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + return null; + } + + /** + * Translate all characters according to the transformation map + * + * @param infile + * the input file + * @param outfile + * the file where the output must be written + */ + public void translate(File infile, File outfile) { + try { + FileWriter writer = new FileWriter(outfile); + String input = toCharSequence(infile).toString(); + String output = translate(input); + + writer.write(output); + writer.flush(); + writer.close(); + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + } + + /** + * Translate (in place) all characters according to the transformation map + * + * @param file + * the input file + * + */ + public void translate(File file) { + try { + FileWriter writer = new FileWriter(file); + String input = toCharSequence(file).toString(); + String output = translate(input); + + writer.write(output); + writer.flush(); + writer.close(); + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/CharMap.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/CharMap.java new file mode 100644 index 00000000..fb4749cd --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/CharMap.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.input.ExtensionFilter; +import eu.digitisation.log.Messages; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + + +/** + * Compares characters and strings according to a mapping between equivalent + * characters + * + * @author R.C.C. + */ +public class CharMap { + + /** + * Typical Options for character comparison + */ + public enum Option { + + /** + * True if case (uppercase or lower) matters + */ + CASE_AWARE, + /** + * True if punctuation marks, currency symbols and all other + * non-letter/non-number symbols matter + */ + PUNCTUATION_AWARE, + /** + * True if diacritics matter + */ + DIACRITICS_AWARE, + /** + * True if Unicode compatibility is active (e.g., between ligatures and + * non-ligatures) + */ + UNICODE_COMPATIBILITY + }; + + EnumMap options; // equivalence options + HashMap equivalences; // specific equivalences for Unicode characters + + /** + * Default constructor: all options true by default + */ + public CharMap() { + options = new EnumMap(Option.class); + for (Option option : Option.values()) { + options.put(option, Boolean.TRUE); + } + equivalences = new HashMap(); + } + + /** + * Constructor with selection of options + * + * @param ops the options whose value must be set to to true (all the other + * being false) + */ + public CharMap(Option[] ops) { + options = new EnumMap(Option.class); + for (Option option : Option.values()) { + options.put(option, Boolean.FALSE); + } + for (Option op : ops) { + options.put(op, Boolean.TRUE); + } + equivalences = new HashMap(); + } + + /** + * Set a value for an option + * + * @param option the option to be set + * @param value its value (true or false) + */ + public void setOption(Option option, boolean value) { + options.put(option, value); + } + + /** + * Read files containing equivalences between characters and sequences + * + * @param file the CSV file (or directory with CSV files) with the + * equivalences between chars and sequences + */ + public void addFilter(File file) { + if (file.isDirectory()) { + String[] filenames = file.list(new ExtensionFilter(".csv")); + for (String filename : filenames) { + addCSV(new File(filename)); + } + } else if (file.isFile()) { + addCSV(file); + } + } + + /** + * Add the equivalences contained in a CSV file + * + * @param file the CSV file + */ + private void addCSV(File file) { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + while (reader.ready()) { + String line = reader.readLine(); + String[] tokens = line.split("([,;\t])"); + if (tokens.length > 1) { // allow comments in line + Character key = (char) Integer.parseInt(tokens[0].trim(), 16); + String value = UnicodeReader.codepointsToString(tokens[1]); + equivalences.put(key, value); + } else { + throw new IOException("Wrong line" + line + + " at file " + file); + } + } + reader.close(); + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + } + + /** + * Normalize characters in a string + * + * @param s a string of characters + * @return the normal form of s for string comparison + */ + public String normalForm(String s) { + String result = s; + System.out.println("S=" + s); + if (!options.get(Option.CASE_AWARE)) { + result = result.toLowerCase(Locale.getDefault()); + } + if (!options.get(Option.DIACRITICS_AWARE)) { + result = StringNormalizer.removeDiacritics(result); + } + if (!options.get(Option.PUNCTUATION_AWARE)) { + // keep only letters, diacritic marks an numbers + result = result.replaceAll("[^\\p{L}\\p{M}\\p{N}]", " "); + } + if (!options.get(Option.UNICODE_COMPATIBILITY)) { + result = StringNormalizer.compatible(result); + } + + for (Map.Entry entry : equivalences.entrySet()) { + result = result.replaceAll(String.valueOf(entry.getKey()), entry.getValue()); + } + + return StringNormalizer.reduceWS(result); + } + + /** + * Normalize a character + * + * @param c a character + * @return the normal form of c for character comparison + */ + public String normalForm(char c) { + return normalForm(String.valueOf(c)); + } + + /** + * Check if two characters are equivalent + * + * @param c1 the first character + * @param c2 the second character + * @return True if c1 is equivalent to c2 + */ + public boolean equiv(char c1, char c2) { + return normalForm(c1).equals(normalForm(c2)); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/Encoding.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/Encoding.java new file mode 100644 index 00000000..4c1db6ff --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/Encoding.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.log.Messages; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.parser.txt.CharsetDetector; + +/** + * Detect the encoding of a text file + * + * @author R.C.C. + */ +public class Encoding { + + /** + * + * @param file a text file + * @return the encoding or Charset + */ + public static Charset detect(File file) { + try { + InputStream is = TikaInputStream.get(new FileInputStream(file)); + CharsetDetector detector = new CharsetDetector(); + detector.setText(is); + return Charset.forName(detector.detect().getName()); + } catch (IOException ex) { + Messages.info(Encoding.class.getName() + ": " + ex); + } + return null; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/StringNormalizer.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/StringNormalizer.java new file mode 100644 index 00000000..76133cdc --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/StringNormalizer.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +/** + * Normalizes strings: collapse whitespace and use composed form (see + * java.text.Normalizer.Form) + * + * @author R.C.C. + */ +public class StringNormalizer { + + final static java.text.Normalizer.Form decomposed = java.text.Normalizer.Form.NFD; + final static java.text.Normalizer.Form composed = java.text.Normalizer.Form.NFC; + static final java.text.Normalizer.Form compatible = java.text.Normalizer.Form.NFKC; + + /** + * Reduce whitespace (including line and paragraph separators) + * + * @param s + * a string. + * @return The string with simple spaces between words. + */ + public static String reduceWS(String s) { + return s.replaceAll("-\n", "").replaceAll( + "(\\p{Space}|\u2028|\u2029)+", " ").trim(); + } + + /** + * @param s + * a string + * @return the canonical representation of the string. + */ + public static String composed(String s) { + return java.text.Normalizer.normalize(s, composed); + } + + /** + * @param s + * a string + * @return the canonical representation of the string with normalized + * compatible characters. + */ + public static String compatible(String s) { + return java.text.Normalizer.normalize(s, compatible); + } + + /** + * @param s + * a string + * @return the string with all diacritics removed. + */ + public static String removeDiacritics(String s) { + return java.text.Normalizer.normalize(s, decomposed) + .replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + } + + /** + * @param s + * a string + * @return the string with all punctuation symbols removed. + */ + public static String removePunctuation(String s) { + return s.replaceAll("\\p{P}+", ""); + } + + /** + * @param s + * a string + * @return the string with leading and trailing whitespace and punctuation + * symbols removed. + */ + public static String trim(String s) { + return s.replaceAll("^(\\p{P}|\\p{Space})+", "") + .replaceAll("(\\p{P}|\\p{Space})+$", ""); + } + + /** + * + * @param s + * the input string + * @param ignoreCase + * true if case is irrelevant + * @param ignoreDiacritics + * true if diacritics are irrelevant + * @param ignorePunctuation + * true if punctuation is irrelevant + * @return the canonical representation for comparison + */ + public static String canonical(String s, + boolean ignoreCase, + boolean ignoreDiacritics, + boolean ignorePunctuation) { + + String res = (ignorePunctuation) ? removePunctuation(s) : s; + if (ignoreCase) { + if (ignoreDiacritics) { + return StringNormalizer.removeDiacritics(res).toLowerCase(); + } else { + return res.toLowerCase(); + } + } else if (ignoreDiacritics) { + return StringNormalizer.removeDiacritics(res); + } else { + return res; + } + } + + /** + * Remove everything except for letters (with diacritics), numbers and + * spaces + * + * @param s + * a string + * @return the string with only letters, numbers, spaces and diacritics. + */ + public static String strip(String s) { + return s.replaceAll("[^\\p{L}\\p{M}\\p{N}\\p{Space}]", ""); + } + + /** + * @param s + * a string + * @return the string with characters <, >, &, " escaped + */ + public static String encode(String s) { + StringBuilder result = new StringBuilder(); + for (Character c : s.toCharArray()) { + if (c.equals('<')) { + result.append("<"); + } else if (c.equals('>')) { + result.append(">"); + } else if (c.equals('"')) { + result.append("""); + } else if (c.equals('&')) { + result.append("&"); + } else { + result.append(c); + } + } + return result.toString(); + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/Text.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/Text.java new file mode 100644 index 00000000..fc5ab2c2 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/Text.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.input.FileType; +import eu.digitisation.input.SchemaLocationException; +import eu.digitisation.input.Settings; +import eu.digitisation.input.WarningException; +import eu.digitisation.layout.SortPageXML; +import eu.digitisation.log.Messages; +import eu.digitisation.xml.DocumentParser; +import eu.digitisation.xml.ElementList; +import eu.digitisation.xml.XPathFilter; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Properties; +import javax.xml.xpath.XPathExpressionException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Extracts the text content in a file. Normalization collapses white-spaces and + * prefers composed form (see java.text.Normalizer.Form). For XML files, + * filtering options can be provided + * + * @author R.C.C. + */ +public class Text { + + StringBuilder builder; + static int maxlen; + Charset encoding; + XPathFilter filter; + + static { + Properties props = Settings.properties(); + maxlen = Integer.parseInt(props.getProperty("maxlen", "0").trim()); + Messages.info("max length of text set to " + maxlen); + try { + File inclusions = new File("inclusions.txt"); + File exclusions = new File("exclusions.txt"); + XPathFilter filter = inclusions.exists() + ? new XPathFilter(inclusions, exclusions) + : null; + } catch (IOException ex) { + Messages.info(Text.class.getName() + ": " + ex); + } catch (XPathExpressionException ex) { + Messages.info(Text.class.getName() + ": " + ex); + } + } + + /** + * Create TextContent from file + * + * @param file + * the input file + * @param encoding + * the text encoding for text files (optional; can be null) + * @param filter + * XPAthFilter for XML files (extracts textual content from + * selected elements) + * @throws eu.digitisation.input.WarningException + * @throws eu.digitisation.input.SchemaLocationException + */ + public Text(File file, Charset encoding, XPathFilter filter) + throws WarningException, SchemaLocationException { + + builder = new StringBuilder(); + this.encoding = encoding; + this.filter = filter; + + try { + FileType type = FileType.valueOf(file); + switch (type) { + case PAGE: + readPageFile(file); + break; + case TEXT: + readTextFile(file); + break; + case FR10: + readFR10File(file); + break; + case HOCR: + readHOCRFile(file); + break; + case ALTO: + readALTOFile(file); + break; + default: + throw new WarningException("Unsupported file format (" + + type + " format) for file " + + file.getName()); + } + } catch (eu.digitisation.input.SchemaLocationException ex) { + throw ex; + } catch (IOException ex) { + Messages.info(Text.class.getName() + ": " + ex); + } + builder.trimToSize(); + } + + /** + * Create Text from file + * + * @param file + * the input file + * @throws eu.digitisation.input.WarningException + * <<<<<<< HEAD + * @throws eu.digitisation.input.SchemaLocationException + * ======= >>>>>>> wip + */ + public Text(File file) + throws WarningException, SchemaLocationException { + this(file, null, null); + } + + /** + * Constructor only for debugging purposes + * + * @param s + * @throws eu.digitisation.input.WarningException + */ + public Text(String s) throws WarningException { + builder = new StringBuilder(); + encoding = Charset.forName("utf8"); + add(s); + } + + /** + * The length of the stored text + * + * @return the length of the stored text + */ + public int length() { + return builder.length(); + } + + /** + * The content as a string + * + * @return the text a String + */ + @Override + public String toString() { + return builder.toString(); + } + + /** + * The content as a string + * + * @param filter + * a CharFilter + * @return the text after the application of the filter + */ + public String toString(CharFilter filter) { + return filter == null + ? builder.toString() + : filter.translate(builder.toString()); + } + + /** + * Add content after normalization of whitespace and composition of + * diacritics + * + * @param s + * input text + */ + private void add(String s) throws WarningException { + String reduced = StringNormalizer.reduceWS(s); + if (reduced.length() > 0) { + String canonical = StringNormalizer.composed(reduced); + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(canonical); + if (maxlen > 0 && builder.length() > maxlen) { + throw new WarningException("Text length limited to " + + maxlen + " characters"); + } + } + } + + private Document loadXMLFile(File file) { + Document doc = DocumentParser.parse(file); + String xmlEncoding = doc.getXmlEncoding(); + + if (xmlEncoding != null) { + encoding = Charset.forName(xmlEncoding); + Messages.info("XML file " + file.getName() + " encoding is " + + encoding); + } else { + if (encoding == null) { + encoding = Encoding.detect(file); + } + Messages.info("No encoding declaration in " + + file + ". Using " + encoding); + } + return doc; + } + + /** + * Read textual content and collapse whitespace: contiguous spaces are + * considered a single one + * + * @param file + * the input text file + */ + private void readTextFile(File file) throws WarningException { + // guess encoding if none is provided + if (encoding == null) { + encoding = Encoding.detect(file); + } + Messages.info("Text file " + file.getName() + " encoding is " + + encoding); + + // read content + try { + FileInputStream fis = new FileInputStream(file); + InputStreamReader isr = new InputStreamReader(fis, encoding); + BufferedReader reader = new BufferedReader(isr); + final StringBuilder completeText = new StringBuilder(); + + String line = null; + while ((line = reader.readLine()) != null) { + completeText.append(line.trim()); + completeText.append('\n'); + } + add(completeText.toString()); + reader.close(); + } catch (IOException ex) { + Messages.info(Text.class.getName() + ": " + ex); + } + } + + /** + * Reads textual content in a PAGE element of type TextRegion + * + * @param region + * the TextRegion element + */ + private void readPageTextRegion(Element region) throws IOException, + WarningException { + NodeList nodes = region.getChildNodes(); + for (int n = 0; n < nodes.getLength(); ++n) { + Node node = nodes.item(n); + if (node.getNodeName().equals("TextEquiv")) { + String text = node.getTextContent(); + add(text); + } + } + } + + /** + * Reads textual content in PAGE XML document. By default selects all + * TextREgion elements + * + * @param file + * the input XML file + */ + private void readPageFile(File file) throws IOException, WarningException { + Document doc = loadXMLFile(file); + Document sorted = SortPageXML.isSorted(doc) ? doc + : SortPageXML.sorted(doc); + List regions = (filter == null) + ? new ElementList(sorted.getElementsByTagName("TextRegion")) + : filter.selectElements(sorted); + + for (int r = 0; r < regions.size(); ++r) { + Element region = regions.get(r); + readPageTextRegion(region); + } + } + + /** + * Reads textual content from FR10 XML paragraph + * + * @param oar + * the paragraph (par) element + */ + private void readFR10Par(Element par) throws WarningException { + NodeList lines = par.getElementsByTagName("line"); + for (int nline = 0; nline < lines.getLength(); ++nline) { + Element line = (Element) lines.item(nline); + StringBuilder text = new StringBuilder(); + NodeList formattings = line.getElementsByTagName("formatting"); + for (int nform = 0; nform < formattings.getLength(); ++nform) { + Element formatting = (Element) formattings.item(nform); + NodeList charParams = formatting.getElementsByTagName("charParams"); + for (int nchar = 0; nchar < charParams.getLength(); ++nchar) { + Element charParam = (Element) charParams.item(nchar); + String content = charParam.getTextContent(); + if (content.length() > 0) { + text.append(content); + } else { + text.append(' '); + } + } + } + add(text.toString()); + } + } + + /** + * Reads textual content from FR10 XML file + * + * @param file + * the input XML file + */ + private void readFR10File(File file) throws WarningException { + Document doc = loadXMLFile(file); + + List pars = (filter == null) + ? new ElementList(doc.getElementsByTagName("par")) + : filter.selectElements(doc); + + for (int npar = 0; npar < pars.size(); ++npar) { + Element par = pars.get(npar); + readFR10Par(par); + } + } + + /** + * Reads textual content from HOCR HTML file + * + * @param file + * the input HTML file + */ + private void readHOCRFile(File file) throws WarningException { + try { + org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parse(file, null); + Charset htmlEncoding = doc.outputSettings().charset(); + + if (htmlEncoding == null) { + encoding = Encoding.detect(file); + Messages.warning("No charset declaration in " + + file + ". Using " + encoding); + } else { + encoding = htmlEncoding; + Messages.info("HTML file " + file + + " encoding is " + encoding); + } + + for (org.jsoup.nodes.Element e : doc.body().select( + "*[class=ocr_line")) { + String text = e.text(); + add(text); + + } + } catch (IOException ex) { + Messages.info(Text.class.getName() + ": " + ex); + } + } + + /** + * Reads textual content in ALTO XML element of type TextLine + * + * @param file + * the input ALTO file + */ + private void readALTOTextLine(Element line) throws WarningException { + NodeList strings = line.getElementsByTagName("String"); + + for (int nstring = 0; nstring < strings.getLength(); ++nstring) { + Element string = (Element) strings.item(nstring); + String text = string.getAttribute("CONTENT"); + add(text); + } + + } + + /** + * Reads textual content from ALTO XML file + * + * @param file + * the input ALTO file + */ + private void readALTOFile(File file) throws WarningException { + Document doc = loadXMLFile(file); + NodeList lines = doc.getElementsByTagName("TextLine"); + + for (int nline = 0; nline < lines.getLength(); ++nline) { + Element line = (Element) lines.item(nline); + readALTOTextLine(line); + } + } + + /** + * Extract text content (under the filtered elements) + * + * @throws java.io.IOException + */ + public static void main(String[] args) throws IOException, + WarningException, XPathExpressionException, SchemaLocationException { + if (args.length < 1 | args[0].equals("-h")) { + System.err.println("usage: Text xmlfile [xpathfile] [xpathfile]"); + } else { + File xmlfile = new File(args[0]); + File inclusions = args.length > 1 ? new File(args[1]) : null; + File exclusions = args.length > 2 ? new File(args[2]) : null; + XPathFilter filter = (inclusions == null) + ? null + : new XPathFilter(inclusions, exclusions); + + Text text = new Text(xmlfile, null, filter); + System.out.println(text); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/UnicodeReader.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/UnicodeReader.java new file mode 100644 index 00000000..1ce17c81 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/UnicodeReader.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.log.Messages; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; + + +/** + * Transformations between Unicode strings and codepoints + * + * @version 2012.06.20 + */ +public class UnicodeReader { + + /** + * Transform a sequence of Unicode values (blocks of four hexadecimal + * digits) into the string they represent. For example, "00410042" (or "0041 + * 0042") represents "AB" + * + * @param codes the sequence of one or more Unicode hex values + * @return the string represented by these codes + * @throws java.io.IOException + */ + protected static String codepointsToString(String codes) throws IOException { + StringBuilder builder = new StringBuilder(); + String[] tokens = codes.trim().split("\\p{Space}+"); + + for (String token : tokens) { + if (token.length() % 4 != 0) { + throw new IOException(token + + " is not a valid Unicode hex sequence"); + } + for (int pos = 0; pos + 3 < token.length(); pos += 4) { + String sub = token.substring(pos, pos + 4); + int val = Integer.parseInt(sub, 16); + builder.append((char) val); + } + } + return builder.toString(); + } + + /** + * Build a string from the codepoints (Unicode values) defining its content + * + * @param codes + * @return the string represented by those codepoints + */ + public static String codepointsToString(int[] codes) { + StringBuilder buff = new StringBuilder(); + for (int code : codes) { + buff.append((char) code); + } + return buff.toString(); + } + + /** + * Convert a string into a sequence of Unicode values + * + * @param s a Java String + * @return The array of Unicode values of the characters in s + */ + public static int[] toCodepoints(String s) { + int[] codes = new int[s.length()]; + for (int n = 0; n < s.length(); ++n) { + codes[n] = (int) s.charAt(n); + } + return codes; + } + + /** + * Transform an array of integers into their hexadecimal representation + * + * @param values an integer array + * @return the hexadecimal strings representing their value. + */ + private static String[] toHexString(int[] values) { + String[] hex = new String[values.length]; + for (int n = 0; n < values.length; ++n) { + hex[n] = Integer.toHexString(values[n]); + } + return hex; + } + + /** + * Convert a string into a sequence of Unicode hexadecimal values + * + * @param s a Java String + * @return The array of Unicode values (hexadecimal representation) of the + * characters in s + */ + public static String[] toHexCodepoints(String s) { + return toHexString(toCodepoints(s)); + } + + /** + * Read a text file and print the content as codepoints (Unicode values) in + * it + * + * @param file the input file + * @throws Exception + */ + public static void printHexCodepoints(File file) + throws Exception { + BufferedReader reader = new BufferedReader(new FileReader(file)); + while (reader.ready()) { + String line = reader.readLine(); + String[] hexcodes = toHexCodepoints(line); + System.out.println(java.util.Arrays.toString(hexcodes)); + } + reader.close(); + } + + /** + * Search for a Unicode sequence and highlight them in browser + * + * @param files files where the sequence is searched + * @param outFile the output file + * @param codepoints the Unicode sequence + */ + public static void find(File[] files, String codepoints, File outFile) { + try { + String pattern = codepointsToString(codepoints); + for (File file : files) { + BufferedReader reader = new BufferedReader(new FileReader(file)); + PrintWriter writer = new PrintWriter(outFile); + writer.print("

" + + file + "

"); + while (reader.ready()) { + String line = reader.readLine(); + if (line.contains(pattern)) { + writer.print("

" + + StringNormalizer.encode(line) + "

"); + } else { + writer.print("

" + StringNormalizer.encode(line) + "

"); + } + } + reader.close(); + writer.close(); + } + } catch (IOException ex) { + Messages.info(CharFilter.class.getName() + ": " + ex); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/WordScanner.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/WordScanner.java new file mode 100644 index 00000000..67773865 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/WordScanner.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple and fast text scanner which reads words from a file and performs the + * tokenization oriented by information-retrieval requirements. + * + * @version 2012.06.20 + */ +public class WordScanner { + + static Pattern pattern; + Matcher matcher; + BufferedReader reader; + + static { + StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append("(\\p{L}+([-\\x26'+/@·.]\\p{L}+)*)"); + builder.append("|"); + builder.append("([\\p{Nd}\\p{Nl}\\p{No}]+([-',./][\\p{Nd}\\p{Nl}\\p{No}]+)*[%‰]?)"); + builder.append(")"); + pattern = Pattern.compile(builder.toString()); + } + + /** + * Open an InputStream for scanning + * + * @param is the InputStream + * @param encoding the character encoding (e.g., UTF-8). + * @param regex the regular expression for words + * @throws IOException + */ + public WordScanner(InputStream is, Charset encoding, String regex) + throws IOException { + InputStreamReader isr = new InputStreamReader(is, encoding); + + if (regex != null) { + pattern = Pattern.compile(regex); + } + + reader = new BufferedReader(isr); + if (reader.ready()) { + matcher = pattern.matcher(reader.readLine()); + } else { + matcher = pattern.matcher(""); + } + } + + /** + * Open an InputStream for scanning + * + * @param is the InputStream + * @param encoding the character encoding (e.g., UTF-8). + * @throws IOException + */ + public WordScanner(InputStream is, Charset encoding) + throws IOException { + this(is, encoding, null); + } + + /** + * Open file with specific encoding for scanning. + * + * @param file the input file. + * @param encoding the encoding (e.g., UTF-8). + * @param regex the regular expression for words + * @throws java.io.IOException + */ + public WordScanner(File file, Charset encoding, String regex) + throws IOException { + this(new FileInputStream(file), encoding, regex); + } + + /** + * Open file with specific encoding for scanning. + * + * @param file the input file. + * @param encoding the encoding (e.g., UTF-8). + * @throws java.io.IOException + */ + public WordScanner(File file, Charset encoding) + throws IOException { + this(new FileInputStream(file), encoding); + } + + /** + * Open a file for scanning. + * + * @param file the input file. + * @throws java.io.IOException + */ + public WordScanner(File file, String regex) + throws IOException { + this(file, Encoding.detect(file), regex); + } + + /** + * Open a string for scanning + * + * @param s the input string to be tokenized + * @param regex the regular expression for words + * @throws IOException + */ + public WordScanner(String s, String regex) throws IOException { + this(new ByteArrayInputStream(s.getBytes("UTF-8")), + Charset.forName("UTF-8"), regex); + } + + /** + * Open a string for scanning + * + * @param s the input string to be tokenized + * @throws IOException + */ + public WordScanner(String s) throws IOException { + this(new ByteArrayInputStream(s.getBytes("UTF-8")), + Charset.forName("UTF-8")); + } + + /** + * + * @param file the input file to be processed + * @param regex the regular expression for words + * @return a StringBuilder with the file content + * @throws java.io.IOException + */ + public static StringBuilder scanToStringBuilder(File file, String regex) + throws IOException { + StringBuilder builder = new StringBuilder(); + WordScanner scanner = new WordScanner(file, regex); + String word; + + while ((word = scanner.nextWord()) != null) { + builder.append(' ').append(word); + } + return builder; + } + + /** + * Returns the next word in file. + * + * @return the next word in the scanned file + * @throws java.io.IOException + */ + public String nextWord() + throws IOException { + String res = null; + while (res == null) { + if (matcher.find()) { + res = matcher.group(0); + } else if (reader.ready()) { + matcher = pattern.matcher(reader.readLine()); + } else { + break; + } + } + return res; + } + + /** + * Sample main. + * + * @param args + */ + public static void main(String[] args) { + WordScanner scanner; + + for (String arg : args) { + try { + String word; + File file = new File(arg); + scanner = new WordScanner(file, "[^\\p{Space}]+"); + while ((word = scanner.nextWord()) != null) { + System.out.println(word); + } + } catch (IOException ex) { + System.err.println(ex); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/text/WordSet.java b/ocrevalUAtion/src/main/java/eu/digitisation/text/WordSet.java new file mode 100644 index 00000000..bf29fa29 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/text/WordSet.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.input.WarningException; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashSet; + +/** + * + * @author R.C.C. + */ +public class WordSet extends HashSet { + + private static final long serialVersionUID = 1L; + + /** + * Default constructor + * + * @param file the file containing the list of stop-words (separated by + * blanks or newlines) + * @throws IOException + */ + public WordSet(File file) throws WarningException { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + while (reader.ready()) { + String line = reader.readLine().trim(); + for (String word : line.split("\\p{Space}+")) { + if (word.length() > 0) { + add(word); + } + } + } + } catch (IOException ex) { + throw new WarningException("File " + file + + " is not a valid stop word file"); + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentBuilder.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentBuilder.java new file mode 100644 index 00000000..d63a4909 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentBuilder.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import eu.digitisation.log.Messages; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Adds some useful auxiliary functions to handle XML documents + * + * @author R.C.C + */ +public class DocumentBuilder { + + Document doc; + + /** + * Create an empty document + * + * @param doctype the document type + */ + public DocumentBuilder(String doctype) { + try { + doc = javax.xml.parsers.DocumentBuilderFactory + .newInstance().newDocumentBuilder() + .newDocument(); + Element root = doc.createElement(doctype); + doc.appendChild(root); + } catch (ParserConfigurationException ex) { + Messages.info(DocumentBuilder.class.getName() + ": " + ex); + } + } + + /** + * Create a copy of another document + * + * @param other + */ + public DocumentBuilder(Document other) { + doc = clone(other); + } + + /** + * Create a copy of another document + * + * @param source a source document + * @return a deep copy of the document + */ + public static Document clone(Document source) { + try { + Document target = javax.xml.parsers.DocumentBuilderFactory + .newInstance().newDocumentBuilder() + .newDocument(); + Node node = target.importNode(source.getDocumentElement(), true); + target.appendChild(node); + return target; + } catch (ParserConfigurationException ex) { + Messages.info(DocumentBuilder.class.getName() + ": " + ex); + } + return null; + } + + /** + * + * @return the org.w3c.dom.Document + */ + public Document document() { + return doc; + } + + /** + * + * @return the root element in this document + */ + public Element root() { + return doc.getDocumentElement(); + } + + /** + * + * @param e The parent element + * @param name The child element name + * @return list of children of e with the given tag + */ + public static List getChildElementsByTagName(Element e, String name) { + ArrayList list = new ArrayList(); + NodeList children = e.getChildNodes(); + + for (int n = 0; n < children.getLength(); ++n) { + Node node = children.item(n); + if (node instanceof Element && node.getNodeName().equals(name)) { + list.add((Element) node); + } + } + return list; + } + + /** + * Create a new element under the designated element in the document. + * + * @param parent the parent element + * @param tag The tag of the new child element + * @return the added element + */ + public Element addElement(Element parent, String tag) { + Element element = doc.createElement(tag); + parent.appendChild(element); + return element; + } + + /** + * Create a new element directly under the root element. + * + * @param tag The tag of the new child element + * @return the added element + */ + public Element addElement(String tag) { + return addElement(root(), tag); + } + + /** + * Insert an element as a child of another element + * + * @param parent the parent element + * @param child the child element (even external one) + * @return the parent element + */ + public Element addElement(Element parent, Element child) { + if (parent.getOwnerDocument() == child.getOwnerDocument()) { + parent.appendChild(child); + } else { + parent.appendChild(doc.importNode(child, true)); + } + return parent; + } + + /** + * Add text content under the given element + * + * @param parent the container element + * @param content the textual content + */ + public void addText(Element parent, String content) { + parent.appendChild(doc.createTextNode(content)); + } + + /** + * Add a text element with the specified textual content under the + * designated element in the document. + * + * @param parent the parent element + * @param tag the new child element tag + * @param content the textual content + * @return the added element + */ + public Element addTextElement(Element parent, String tag, String content) { + Element element = doc.createElement(tag); + element.appendChild(doc.createTextNode(content)); + parent.appendChild(element); + return element; + } + + /** + * Dump content to string + * + * @return the content as a string + */ + @Override + public String toString() { + return new DocumentWriter(doc).toString(); + } + + /** + * Dump content to file + * + * @param file the output file + */ + public void write(java.io.File file) { + new DocumentWriter(doc).write(file); + } + +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentParser.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentParser.java new file mode 100644 index 00000000..73f9dc3d --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentParser.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import eu.digitisation.log.Messages; +import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * A builder and parser for XML documents + * + * @author R.C.C. + * @version 2011.03.10 + */ +public class DocumentParser { + + static javax.xml.parsers.DocumentBuilder docBuilder; + + static { + try { + docBuilder = javax.xml.parsers.DocumentBuilderFactory + .newInstance().newDocumentBuilder(); + } catch (ParserConfigurationException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } + } + + /** + * Create XML document from file content + * + * @param file the input file + * @return an XML document + */ + public static Document parse(java.io.File file) { + try { + return docBuilder.parse(file); + } catch (SAXException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } catch (IOException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } + return null; + } + + /** + * Create XML document from InputStream content + * + * @param is the InputStream with XML content + * @return an XML document + */ + public static Document parse(java.io.InputStream is) { + try { + return docBuilder.parse(is); + } catch (SAXException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } catch (IOException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } + + return null; + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentWriter.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentWriter.java new file mode 100644 index 00000000..08faf9b8 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/DocumentWriter.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import eu.digitisation.log.Messages; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import org.w3c.dom.Document; + +/** + * Writes XML document to String or File + * + * @author R.C.C. + */ +public class DocumentWriter { + + static javax.xml.transform.Transformer transformer; + + javax.xml.transform.dom.DOMSource source; + javax.xml.transform.stream.StreamResult result; + + static { + try { + transformer = javax.xml.transform.TransformerFactory + .newInstance().newTransformer(); + } catch (TransformerConfigurationException ex) { + Messages.info(DocumentWriter.class.getName() + ": " + ex); + } + } + + /** + * Create a DocumentWriter for a given document + * + * @param document the XML document + */ + public DocumentWriter(Document document) { + source = new javax.xml.transform.dom.DOMSource(document); + } + + /** + * Write XML to string + * + * @return string representation + */ + @Override + public String toString() { + result = new javax.xml.transform.stream.StreamResult(new java.io.StringWriter()); + try { + transformer.transform(source, result); + } catch (TransformerException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } + return result.getWriter().toString(); + } + + /** + * Dump content to file + * + * @param file the output file + */ + public void write(java.io.File file) { + result = new javax.xml.transform.stream.StreamResult(file); + try { + transformer.transform(source, result); + } catch (TransformerException ex) { + Messages.info(DocumentParser.class.getName() + ": " + ex); + } + + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/ElementList.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/ElementList.java new file mode 100644 index 00000000..20c4bf89 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/ElementList.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import java.util.ArrayList; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A class transforming a org.w3c.dom.NodeList into a list of elements + * + * @author R.C.C + */ +public class ElementList extends ArrayList { + + /** + * Create a list of elements from a org.w3c.dom.NodeList + * + * @param nodes + */ + public ElementList(NodeList nodes) { + for (int n = 0; n < nodes.getLength(); ++n) { + Node node = nodes.item(n); + if (node instanceof Element) { + add((Element) node); + } else { + throw new IllegalArgumentException("ElementList: " + + "source NodeList contains non-element nodes"); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/XML2text.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XML2text.java new file mode 100644 index 00000000..d886126a --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XML2text.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import eu.digitisation.input.Parameters; +import eu.digitisation.log.Messages; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Removes markup, declarations, PI's and comments from XML files. Implemented + * as a SAX parser. + */ +public class XML2text extends DefaultHandler { + + static final String helpMsg = "Usage:\t" + + "java -cp target/ocrevaluation.jar eu.digitisation.xml.XML2text -in file1" + + " -o output_file_or_dir"; + + private static void exit_gracefully() { + System.err.println(helpMsg); + System.exit(0); + } + + private StringBuilder buffer; + private static final FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".xml"); + } + }; + + @Override + public void characters(char[] c, int start, int length) { + if (length > 0) { + buffer.append(c, start, length); + } + } + + @Override + public void startElement(String uri, String localName, + String tag, Attributes attributes) { + buffer.append(" "); + } + + @Override + public void endElement(String uri, String localName, String tag) { + buffer.append(" "); + } + + private XMLReader getXMLReader() { + XMLReader reader = null; + try { + reader = SAXParserFactory.newInstance() + .newSAXParser().getXMLReader(); + reader.setContentHandler(this); + + } catch (SAXException ex) { + Messages.info(XML2text.class.getName() + ": " + ex); + } catch (ParserConfigurationException ex) { + Messages.info(XML2text.class.getName() + ": " + ex); + } + return reader; + } + + /** + * Read file and return text content. + * + * @param fileName the name of the file. + * @return text in file without markup. + */ + public String getText(String fileName) { + XMLReader reader = getXMLReader(); + buffer = new StringBuilder(10000); + try { + reader.parse(fileName); + } catch (IOException ex) { + Messages.info(XML2text.class.getName() + ": " + ex); + } catch (SAXException ex) { + Messages.info(XML2text.class.getName() + ": " + ex); + } + return buffer.toString(); + } + + /** + * Main function + * + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + XML2text xml = new XML2text(); + String outDir = null; + Parameters pars = new Parameters(); + + for (int n = 0; n < args.length; ++n) { + String arg = args[n]; + if (arg.equals("-o")) { + pars.outfile.setValue(new File(args[++n])); + } else if (arg.equals("-in")) { + pars.ocrfile.setValue(new File(args[++n])); + }else { + System.err.println("Unrecognized option " + arg); + System.err.println("usage: XML2text [-o outfile]" + + "file1.xml file2.xml..."); + } + } + + if (pars.ocrfile.getValue() == null || pars.ocrfile.getValue() == null) { + System.err.println("Not enough arguments"); + exit_gracefully(); + } + + if (pars.outfile.getValue() == null) { + String name = pars.ocrfile.getValue().getName().replaceAll("\\.\\w+", "") + + ".txt"; + pars.outfile.setValue(new File(pars.ocrfile.getValue().getParent(), name)); + exit_gracefully(); + } + + File infile = new File(pars.ocrfile.getValue().toString()); + String outfileName = pars.outfile.getValue().toString(); + + File outfile = new File(outDir, outfileName); + if (outfile.exists()) { + System.err.println(outfileName + "already exists "); + } else { + BufferedWriter writer = + new BufferedWriter(new FileWriter(outfileName)); + writer.write(xml.getText(infile.getAbsolutePath())); + writer.close(); + } + //pars.outfile.getValue() + + + /* + for (int n = 0; n < args.length; ++n) { + String arg = args[n]; + + if (arg.equals("-d")) { + outDir = args[++n]; + if (! new File(outDir).mkdir()) { + throw new IOException("Unable to create dir " + outDir); + } + } else if (arg.endsWith(".xml")) { + File infile = new File(arg); + String outfileName = arg.replace(".xml", ".txt"); + File outfile = new File(outDir, outfileName); + if (outfile.exists()) { + System.err.println(outfileName + "already exists "); + } else { + BufferedWriter writer = + new BufferedWriter(new FileWriter(outfileName)); + writer.write(xml.getText(infile.getAbsolutePath())); + writer.close(); + } + } + } + */ + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/XMLPath.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XMLPath.java new file mode 100644 index 00000000..9752aa11 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XMLPath.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import eu.digitisation.log.Messages; +import java.io.File; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Evaluate XPath expressions. + */ +public class XMLPath { + + final static XPath xpath; + + static { + xpath = XPathFactory.newInstance().newXPath(); + } + + /** + * Evaluate XPath expression + * + * @param doc the container document + * @param expression XPath expression + * @return the list of nodes matching the query + */ + public static NodeList evaluate(Document doc, String expression) { + try { + return (NodeList) xpath.evaluate(expression, doc, + XPathConstants.NODESET); + } catch (XPathExpressionException ex) { + Messages.info(XMLPath.class.getName() + ": " + ex); + } + return null; + } + + /** + * Evaluate XPath expression + * + * @param file the file containing the XML document + * @param expression XPath expression + * @return the list of nodes matching the query + */ + public static NodeList evaluate(File file, String expression) { + Document doc = DocumentParser.parse(file); + try { + return (NodeList) xpath.evaluate(expression, doc, + XPathConstants.NODESET); + } catch (XPathExpressionException ex) { + Messages.info(XMLPath.class.getName() + ": " + ex); + } + return null; + } + + /** + * + * @param element an XML element + * @param expression an XPath expression + * @return the list of descendent nodes matching the query + */ + public static NodeList evaluate(Element element, String expression) { + try { + return (NodeList) xpath.evaluate(expression, element, + XPathConstants.NODESET); + } catch (XPathExpressionException ex) { + Messages.info(XMLPath.class.getName() + ": " + ex); + } + return null; + } + + /** + * Test if an element matches the given expression + * + * @param element an XML element + * @param expression an XPath expression with respect to the element it self + * (e.g., an element tag) + * @return true if the given element matches the query + */ + public static boolean matches(Element element, String expression) { + try { + return (Boolean) xpath.evaluate("self::" + expression, element, XPathConstants.BOOLEAN); + } catch (XPathExpressionException ex) { + Messages.info(XMLPath.class.getName() + ": " + ex); + } + return false; + } + + /** + * Sample main + * + * @param args + */ + public static void main(String[] args) { + if (args.length != 2) { + System.err.println("usage: XMLpath filename xpath"); + } else { + File file = new File(args[0]); + String expr = args[1]; + try { + NodeList nodes = XMLPath.evaluate(file, expr); + System.out.println(nodes.getLength()); + for (int n = 0; n < nodes.getLength(); ++n) { + System.out.println(nodes.item(n)); + } + } catch (Exception e) { + System.err.println(e); + } + } + } +} diff --git a/ocrevalUAtion/src/main/java/eu/digitisation/xml/XPathFilter.java b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XPathFilter.java new file mode 100644 index 00000000..f7162703 --- /dev/null +++ b/ocrevalUAtion/src/main/java/eu/digitisation/xml/XPathFilter.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.xml; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Test elements against XPath expressions and include or exclude the elements + * + * @author R.C.C. + */ +public class XPathFilter { + + static XPath xpath = XPathFactory.newInstance().newXPath(); + List inexpr; // the inclussion expressions + List exexpr; // the exclussion expressions + List inclusions; // XPath expressions of included elements + List exclusions; // XPath expressions of excluded elements + + private void include(String expression) throws XPathExpressionException { + inexpr.add(expression); + inclusions.add(xpath.compile("self::" + expression)); + } + + private void includeAll(String[] array) throws XPathExpressionException { + if (array != null) { + for (String s : array) { + include(s); + } + } + } + + private void exclude(String expression) throws XPathExpressionException { + exexpr.add(expression); + exclusions.add(xpath.compile("self::" + expression)); + } + + private void excludeAll(String[] array) throws XPathExpressionException { + if (array != null) { + for (String s : array) { + exclude(s); + } + } + } + + /** + * Default constructor + */ + public XPathFilter() { + inexpr = new ArrayList(); + exexpr = new ArrayList(); + inclusions = new ArrayList(); + exclusions = new ArrayList(); + } + + /** + * Create an XPathFilter from two arrays of XPath expressions + * + * @param inclusions an array of XPath inclusion expressions (possibly null) + * @param exclusions array of XPath exclusion expressions (possibly null) + * @throws javax.xml.xpath.XPathExpressionException + */ + public XPathFilter(String[] inclusions, String[] exclusions) + throws XPathExpressionException { + this(); + includeAll(inclusions); + excludeAll(exclusions); + } + + /** + * Read file into lines + * + * @param file the input file + * @return the content as a list of strings, each one with the content in + * one file line, excluding those starting with the character # + * @throws IOException + */ + private String[] lines(File file) throws IOException { + List list = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(file)); + while (reader.ready()) { + String line = reader.readLine().trim(); + if (line.length() > 0 && !line.startsWith("#")) { + list.add(line); + } + } + return list.toArray(new String[list.size()]); + } + + /** + * Create an ElementFilter from the XPath expressions in a file (one per + * line) + * + * @param infile a file containing XPath inclusion expressions (one per + * line) + * @param exfile a file containing XPath exclusion expressions (one per + * line) + * @throws java.io.IOException + * @throws javax.xml.xpath.XPathExpressionException + */ + public XPathFilter(File infile, File exfile) + throws IOException, XPathExpressionException { + this(); + if (infile != null) { + includeAll(lines(infile)); + } + if (exfile != null) { + excludeAll(lines(exfile)); + } + } + + /** + * Check if the element matches any valid inclusion expression + * + * @param element an XML element + * @return true if the element matches any of the XPath inclusion + * expressions + */ + public boolean included(Element element) { + + for (int n = 0; n < inclusions.size(); ++n) { + XPathExpression expression = inclusions.get(n); + try { + Boolean match = (Boolean) expression.evaluate(element, + XPathConstants.BOOLEAN); + if (match) { + return true; + } + } catch (XPathExpressionException ex) { + // not a valid match + } + } + return false; + } + + /** + * Check if the element matches any valid inclusion expression + * + * @param element an XML element + * @return true if the element matches any of the XPAth exclusion + * expressions + */ + public boolean excluded(Element element) { + for (XPathExpression expression : exclusions) { + try { + Boolean match = (Boolean) expression.evaluate(element, + XPathConstants.BOOLEAN); + if (match) { + return true; + } + } catch (XPathExpressionException ex) { + // not a valid match + } + } + return false; + } + + /** + * + * @param element an XML element + * @return true if the element matches any of the XPAth inclusion + * expressions and none of the exclusion expressions + */ + private boolean accepted(Element element) { + return included(element) && !excluded(element); + } + + /** + * Select elements matching the XPath valid expression + * + * @param element a parent element + * @return all descendent elements matching at least one of the XPath + * expressions in this filter + */ + public List selectElements(Element element) { + NodeList nodeList = element.getElementsByTagName("*"); + List selection = new ArrayList(); + + for (int n = 0; n < nodeList.getLength(); n++) { + Node node = nodeList.item(n); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + if (accepted(e)) { + selection.add(e); + } + } + } + return selection; + } + + /** + * Select elements matching the XPath valid expression + * + * @param doc a container XML document + * @return all elements in the document matching at least one of the XPath + * expressions in this filter + */ + public List selectElements(Document doc) { + NodeList nodeList = doc.getElementsByTagName("*"); + List selection = new ArrayList(); + + for (int n = 0; n < nodeList.getLength(); n++) { + Node node = nodeList.item(n); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) node; + if (accepted(e)) { + selection.add(e); + } + } + } + return selection; +// return selectElements(doc.getDocumentElement()); + } + + /** + * Select content under the filtered elements + * + * @param args + * @throws java.io.IOException + * @throws javax.xml.xpath.XPathExpressionException + */ + public static void main(String[] args) throws IOException, XPathExpressionException { + if (args.length < 2) { + System.err.println("usage: XPathFilter xmlfile xpathfile xpathfile"); + } else { + File xmlfile = new File(args[0]); + File xpathinfile = new File(args[1]); + File xpathexfile = new File(args[2]); + XPathFilter filter = new XPathFilter(xpathinfile, xpathexfile); + List selected = filter.selectElements(DocumentParser.parse(xmlfile)); + for (Element e : selected) { + System.out.println(e.getNodeName()); + } + } + } +} diff --git a/ocrevalUAtion/src/main/resources/Image.properties b/ocrevalUAtion/src/main/resources/Image.properties new file mode 100644 index 00000000..62a68bd6 --- /dev/null +++ b/ocrevalUAtion/src/main/resources/Image.properties @@ -0,0 +1,2 @@ +# minimal value of y-projection +line.thrshold=-0.5 \ No newline at end of file diff --git a/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.csv b/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.csv new file mode 100644 index 00000000..2ff57167 --- /dev/null +++ b/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.csv @@ -0,0 +1,54 @@ +00C6 0041 0045 +0132 0049 004A +0133 0069 006A +0153 006F 0065 +A76D 0069 0073 +E1DC 00D1 +E42C 0061 0364 +E5B8 006D 0304 +E5DC 00F1 +E665 0070 0304 +E681 0071 0304 +E682 0071 0307 +E6E2 0074 0301 +EADA 017F 0074 +EBA2 017F 0069 +EBA3 017F 006C +EBA6 017F 017F +EBA7 017F 017F 0069 +EBE3 006A 0308 +EEC4 0063 006B +EEC5 0063 0074 +EEDC 0074 007A +EFA1 0061 0065 +F161 003B +F1AC 003B +F4F9 006C 006C +F4FF 017F 0074 +F501 0063 0304 +F502 0063 0068 +F503 1EBD +F504 0067 030A +F505 0067 0304 +F506 0068 030A +F507 0070 0304 +F50A 0064 0027 +F50B 006C 0027 +F50D 0071 0301 A76B +F50E 0071 0301 +F50F 0071 0303 +F510 0072 0304 +F511 0073 0303 +F512 0074 0303 +F515 0065 0074 +F517 0063 0303 +F518 0072 0303 +F519 006D 0303 +F51E 017F 0142 +FB00 0066 0066 +FB01 0066 0069 +FB02 0066 006C +FB03 0066 0066 0069 +FB04 0066 0066 006C +FB06 0073 0074 +FEFF 0020 # Byte order mark diff --git a/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.txt b/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.txt new file mode 100644 index 00000000..862b62df --- /dev/null +++ b/ocrevalUAtion/src/main/resources/UnicodeCharEquivalences.txt @@ -0,0 +1,54 @@ +FEFF, 0020, # Byte order mark +F1AC, 003B +EFA1, 00E6 +EEC4, 0063 006B +EEC5, 0063 0074 +F502, 0063 0068 +F517, 0063 0303 +F50A, 0064 0027 +F515, 0065 0074 +FB00, 0066 0066 +FB01, 0066 0069 +FB02, 0066 006C +F504, 0067 030A +F505, 0067 0304 +F506, 0068 030A +0133, 0069 006A +A76D, 0069 0073 +EBE3, 006A 0308 +F4F9, 006C 006C +F50B, 006C 0027 +E5B8, 006D 0304 +F519, 006D 0303 +E1DC, 00D1 +E5DC, 00F1 +F50F, 0071 0303 +F510, 0072 0304 +F518, 0072 0303 +EADA, 017F 0074 +EBA2, 017F 0069 +EBA6, 017F 017F +EBA7, 017F 017F 0069 +F4FF, 017F 0074 +F511, 0073 0303 +F51E, 017F 0142 +FB06, 0073 0074 +E6E2, 0074 0301 +EEDC, 0074 007A +F512, 0074 0303 +F161, 003B +F503, 1EBD +F507, 0070 0304 +00C6, 0041 0045 +0132, 0049 004A +0153, 006F 0065 +E42C, 0061 0364 +E665, 0070 0304 +E681, 0071 0304 +E682, 0071 0307 +EBA3, 017F 006C +F501, 0063 0304 +F50D, 0071 0301 A76B +F50E, 0071 0301 +FB03, 0066 0066 0069 +FB04, 0066 0066 006C \ No newline at end of file diff --git a/ocrevalUAtion/src/main/resources/defaultProperties.xml b/ocrevalUAtion/src/main/resources/defaultProperties.xml new file mode 100644 index 00000000..cc985325 --- /dev/null +++ b/ocrevalUAtion/src/main/resources/defaultProperties.xml @@ -0,0 +1,23 @@ + + + + + + 0 + + + + http://schema.primaresearch.org/PAGE/gts/pagecontent/2010-03-19 + http://schema.primaresearch.org/PAGE/gts/pagecontent/2010-03-19/pagecontent.xsd + http://schema.primaresearch.org/PAGE/gts/pagecontent/2013-07-15 + http://schema.primaresearch.org/PAGE/gts/pagecontent/2013-07-15/pagecontent.xsd + + + http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml + + + http://www.loc.gov/standards/alto/alto-v2.0.xsd + http://schema.ccs-gmbh.com/ALTO + + \ No newline at end of file diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/ArrayEditDistanceTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/ArrayEditDistanceTest.java new file mode 100644 index 00000000..c643e233 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/ArrayEditDistanceTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package eu.digitisation.distance; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class ArrayEditDistanceTest { + + /** + * Test of indel method, of class ArrayEditDistance. + */ + @Test + public void testIndelDistance() { + System.out.println("indelDistance"); + Object[] first = {'p','a','t','a','t','a'}; + Object[] second = {'a','p','t','a'}; + int expResult = 4; + int result = ArrayEditDistance.indel(first, second); + assertEquals(expResult, result); + } + + /** + * Test of levenshtein method, of class ArrayEditDistance. + */ + @Test + public void testLevenshteinDistance() { + System.out.println("levenshteinDistance"); + Object[] first = {'p','a','t','a','t','a'}; + Object[] second = {'a','p','t','a'}; + int expResult = 3; + int result = ArrayEditDistance.levenshtein(first, second); + assertEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditDistanceTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditDistanceTest.java new file mode 100644 index 00000000..2a41fe6c --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditDistanceTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 Uni. de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.text.Text; +import java.io.File; +import java.net.URL; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class EditDistanceTest { + + @Test + public void testWeights() { + System.out.println("Weighted distance"); + EdOpWeight w = new OcrOpWeight(); + String s1 = "a b"; + String s2 = "acb"; + int expResult = 2; + int result = EditDistance.charDistance(s1, s2, w, 50); + assertEquals(expResult, result); + } + + /** + * Test of wordDistance method, of class EditDistance. + */ + @Test + public void testWordDistance() { + System.out.println("wordDistance"); + String s1 = "p a t a t a"; + String s2 = "a p t a"; + int expResult = 3; + int[] result = EditDistance.wordDistance(s1, s2, 10); + assertEquals(expResult, result[2]); + } + + @Test + public void testWeightedDistance() { + String s1 = "ÁÁÁÁ"; + String s2 = "ÁAáa"; + + OcrOpWeight W = new OcrOpWeight(); // fully-sensitive + String r1 = StringNormalizer.canonical(s1, false, false, false); + String r2 = StringNormalizer.canonical(s2, false, false, false); + assertEquals(3, EditDistance.charDistance(r1, r2, W, 1000)); + + W = new OcrOpWeight(true); //ignore everything + r1 = StringNormalizer.canonical(s1, true, true, true); + r2 = StringNormalizer.canonical(s2, true, true, true); + assertEquals(0, EditDistance.charDistance(r1, r2, W, 1000)); + + W = new OcrOpWeight(true); //ignore diacritics + r1 = StringNormalizer.canonical(s1, false, true, true); + r2 = StringNormalizer.canonical(s2, false, true, true); + assertEquals(2, EditDistance.charDistance(r1, r2, W, 1000)); + + W = new OcrOpWeight(true); //ignore case + r1 = StringNormalizer.canonical(s1, true, false, true); + r2 = StringNormalizer.canonical(s2, true, false, true); + assertEquals(2, EditDistance.charDistance(r1, r2, W, 1000)); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditSequenceTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditSequenceTest.java new file mode 100644 index 00000000..33d5948a --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditSequenceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.math.BiCounter; +import eu.digitisation.output.CharStatTable; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class EditSequenceTest { + + /** + * Test of cost method, of class EditSequence. + */ + @Test + public void testCost() { + System.out.println("cost"); + EditSequence instance = new EditSequence("acb", "a b", new OcrOpWeight()); + int expResult = 2; + int result = instance.length(); + assertEquals(expResult, result); + } + + /** + * Test of shift1 method, of class EditSequence. + */ + @Test + public void testShift1() { + System.out.println("shift1"); + EditSequence instance = new EditSequence("acb", "a b", new OcrOpWeight()); + int expResult = 3; + int result = instance.shift1(); + assertEquals(expResult, result); + } + + @Test + public void testStats() { + System.out.println("stats"); + String s1 = "acb"; + String s2 = "abs"; + EditSequence instance = new EditSequence(s1, s2, new OcrOpWeight()); + BiCounter result = instance.stats(s1, s2); + BiCounter expResult = new BiCounter(); + expResult.add('a', EdOp.KEEP, 1); + expResult.add('b', EdOp.KEEP, 1); + expResult.add('c', EdOp.DELETE, 1); + expResult.add('s', EdOp.INSERT, 1); + assertEquals(expResult, result); + + EdOpWeight w = new OcrOpWeight(); + result = instance.stats(s1, s2, w); + assertEquals(expResult, result); + } + + @Test + public void testPunct() { + OcrOpWeight w = new OcrOpWeight(true); /// ignore punctuation + String s1 = "yes ! , he said"; + String s2 = "yes he said"; + EditSequence edit = new EditSequence(s1, s2, new OcrOpWeight()); + CharStatTable stats = new CharStatTable(); + stats.add(edit.stats(s1, s2)); + System.out.println(stats); + double cer = stats.cer(); + + assertEquals(0, 0, 0.00001); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditTableTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditTableTest.java new file mode 100644 index 00000000..8246152b --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/EditTableTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class EditTableTest { + + public EditTableTest() { + } +/* + @Test + public void testSet() { + System.out.println("set"); + byte b = 0; + byte result = EditTable.setBit(b, 0, true); + System.out.println(result); + assertEquals(1, result); + assertEquals(true, EditTable.getBit(result,0)); + } +*/ + /** + * Test of get method, of class EditTable. + */ + @Test + public void testGet() { + System.out.println("get"); + EditTable instance = new EditTable(2, 2); + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + if (i == j) { + instance.set(i, j, EdOp.KEEP); + } else { + instance.set(i, j, EdOp.SUBSTITUTE); + } + } + } + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + if (i == j) { + assertEquals(EdOp.KEEP, instance.get(i, j)); + } else { + assertEquals(EdOp.SUBSTITUTE, instance.get(i, j)); + } + } + } + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/OcrOpWeightTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/OcrOpWeightTest.java new file mode 100644 index 00000000..3542f635 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/OcrOpWeightTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.text.StringNormalizer; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class OcrOpWeightTest { + + /** + * Test of sub method, of class OcrWeights. + */ + @Test + public void testSub() { + System.out.println("sub"); + char[] c1 = {'Á', 'Á', 'Á', 'Á', 'Á'}; + char[] c2 = {'Á', 'A', 'á', 'a', ' '}; + int[] w1 = {0, 1, 1, 1, 2}; + OcrOpWeight W1 = new OcrOpWeight(); // fully-sensitive + for (int n = 0; n < w1.length; ++n) { + String s1 = StringNormalizer.canonical(String.valueOf(c1[n]), false, false, false); + String s2 = StringNormalizer.canonical(String.valueOf(c2[n]), false, false, false); + int d = EditDistance.charDistance(s1, s2, W1, 10); + assertEquals(w1[n], d); + } + OcrOpWeight W2 = new OcrOpWeight(true); //ignore everything + int[] w2 = {0, 0, 0, 0, 2}; + for (int n = 0; n < w2.length; ++n) { + String s1 = StringNormalizer.canonical(String.valueOf(c1[n]), true, true, true); + String s2 = StringNormalizer.canonical(String.valueOf(c2[n]), true, true, true); + int d = EditDistance.charDistance(s1, s2, W2, 10); + assertEquals(w2[n], d); + } + OcrOpWeight W3 = new OcrOpWeight(false); //ignore diacritics + int[] w3 = {0, 0, 1, 1, 2}; + for (int n = 0; n < w3.length; ++n) { + String s1 = StringNormalizer.canonical(String.valueOf(c1[n]), false, true, true); + String s2 = StringNormalizer.canonical(String.valueOf(c2[n]), false, true, true); + int d = EditDistance.charDistance(s1, s2, W3, 10); + assertEquals(w3[n], d); + } + + OcrOpWeight W4 = new OcrOpWeight(true); //ignore case + int[] w4 = {0, 1, 0, 1, 2}; + for (int n = 0; n < w4.length; ++n) { + String s1 = StringNormalizer.canonical(String.valueOf(c1[n]), true, false, true); + String s2 = StringNormalizer.canonical(String.valueOf(c2[n]), true, false, true); + int d = EditDistance.charDistance(s1, s2, W4, 10); + assertEquals(w4[n], d); + } + } + + /** + * Test of ins method, of class OcrWeights. + */ + @Test + public void testIns() { + System.out.println("ins"); + OcrOpWeight W = new OcrOpWeight(true); //ignore punct + assertEquals(1, W.ins('a')); + assertEquals(0, W.ins('@')); + assertEquals(0, W.ins('+')); + W = new OcrOpWeight(); + assertEquals(1, W.ins('a')); + assertEquals(1, W.ins('@')); + assertEquals(1, W.ins('+')); + + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/StringEditDistanceTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/StringEditDistanceTest.java new file mode 100644 index 00000000..63f9fa69 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/StringEditDistanceTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import eu.digitisation.text.StringNormalizer; +import eu.digitisation.math.BiCounter; +import static junit.framework.TestCase.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class StringEditDistanceTest { + + public StringEditDistanceTest() { + } + + /** + * Test of indel method, of class StringEditDistance. + */ + @Test + public void testIndelDistance() { + System.out.println("indelDistance"); + String first = "patata"; + String second = "apta"; + int expResult = 4; + int result = StringEditDistance.indel(first, second); + assertEquals(expResult, result); + + } + + /** + * Test of levenshtein method, of class StringEditDistance. + */ + @Test + public void testLevenshteinDistance() { + System.out.println("levenshteinDistance"); + String first = "patata"; + String second = "apta"; + int expResult = 3; + int result = StringEditDistance.levenshtein(first, second); + assertEquals(expResult, result); + // A second test + first = "holanda"; + second = "wordland"; + result = StringEditDistance.levenshtein(first, second); + assertEquals(4, result); + // Test with normalization + first = StringNormalizer.reduceWS("Mi enhorabuena"); + second = StringNormalizer.reduceWS("mi en hora buena"); + result = StringEditDistance.levenshtein(first, second); + assertEquals(3, result); + } + + + /** + * Test of DL method, of class StringEditDistance. + */ + @Test + public void testDLDistance() { + System.out.println("Damerau-Levenshtein Distance"); + String first = "abracadabra"; + String second = "arbadacarba"; + int expResult = 4; + int result = StringEditDistance.DL(first, second); + assertEquals(expResult, result); + + } + + @Test + public void testOperations() { + System.out.println("operations"); + String first = "patata"; + String second = "apta"; + BiCounter expResult = new BiCounter(); + expResult.add('a', EdOp.KEEP, 2); // sure + expResult.inc('t', EdOp.KEEP); // sure + expResult.inc('p', EdOp.DELETE); // sure + expResult.inc('t', EdOp.SUBSTITUTE); // not the ony pssibility + expResult.inc('a', EdOp.DELETE); // could exchange with 'a' above + + BiCounter result + = StringEditDistance.operations(first, second); + System.out.println(result); + assertEquals(expResult, result); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/distance/WordCompareTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/distance/WordCompareTest.java new file mode 100644 index 00000000..a2ac7b3b --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/distance/WordCompareTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.distance; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class WordCompareTest { + + /** + * Test of wdiff method, of class WordCompare. + */ + @Test + public void testWdiff() { + System.out.println("wdiff"); + String first = "one to just one"; + String second = "to see one"; + String expResult = "one # []\nto = to\njust # see\none = one\n"; + String result = WordCompare.wdiff(first, second); + assertEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/document/TermFrequencyVectorTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/document/TermFrequencyVectorTest.java new file mode 100644 index 00000000..aa9138c3 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/document/TermFrequencyVectorTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.document; + +import eu.digitisation.distance.EditDistance; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class TermFrequencyVectorTest { + + String s1 = //"UN AN : PARIS, 8 Francs. — PROVINCE, 10 Francs. — ETRANGER, suivant le Tarif postal. " + "A LA LIBRAIRIE, 10, RUE DE LA BOURSE. CHRONIQUE GOURMANDE UNE des gracieuses"; + String s2 = //"V AN : PA's»s*c8fFrancs. — Pr«vjnv-e, 11 > Fr.it:-*.— K: kvnobi. ', Tarif ;\".s:a!. 1- ni 7 + "A LA LIBRAIRIE, 10. RUE DE LA BOURSE. TOUS PREMIER. I I VRAIS'< , CHRONIQUE GOURMANDE * ,' -~J.,' 1 Ii nk .!•'« gracieuses"; + + public TermFrequencyVectorTest() { + + } + + /** + * Test of distance method, of class TermFrequencyVector. + */ + @Test + public void testDistance() { + System.out.println("distance"); + TermFrequencyVector tf1 = new TermFrequencyVector(s1); + TermFrequencyVector tf2 = new TermFrequencyVector(s2); + int expResult = EditDistance.wordDistance(s1, s2, 1000)[2]; + int result = tf1.distance(tf2); + assertEquals(expResult, result); + } + + /** + * Test of total method, of class TermFrequencyVector. + */ + @Test + public void testTotal() { + System.out.println("total"); + TermFrequencyVector tf1 = new TermFrequencyVector(s1); + TermFrequencyVector tf2 = new TermFrequencyVector(s2); + System.out.println("tf1=" + tf1.toString()); + System.out.println("f2=" + tf2.toString()); + assertEquals(13, tf1.total()); + assertEquals(20, tf2.total()); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/document/TokenArrayTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/document/TokenArrayTest.java new file mode 100644 index 00000000..8b9b2b91 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/document/TokenArrayTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.document; + +import eu.digitisation.math.MinimalPerfectHash; +import static junit.framework.TestCase.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class TokenArrayTest { + + public TokenArrayTest() { + } + + + + /** + * Test of encode method, of class TextFileEncoder. + */ + @Test + public void testEncode_String() { + System.out.println("encode"); + String input = "hola&amigo2\n3.14 mi casa, todos los días\n" + + "mesa-camilla java4you i.b.m. i+d Dª María 3+100%"; + String expOutput = "hola&amigo 2 3.14 mi casa todos los días" + + " mesa-camilla java 4 you i.b.m i+d Dª María 3 100%"; + MinimalPerfectHash f = new MinimalPerfectHash(true); + TokenArray array = new TokenArray(f, input); + String output = array.toString(); + assertEquals(expOutput, output); + + int size = array.length(); + assertEquals(18, size); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/input/BatchTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/input/BatchTest.java new file mode 100644 index 00000000..a53602a7 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/input/BatchTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.input; + +import eu.digitisation.input.Batch; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author R.C.C + */ +public class BatchTest { + + public BatchTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of prefix method, of class Batch. + */ + @Test + public void testLcp() { + System.out.println("lcp"); + String s1 = "compare"; + String s2 = "competence"; + String expResult = "comp"; + String result = Batch.prefix(s1, s2); + assertEquals(expResult, result); + } + + /** + * Test of suffix method, of class Batch. + */ + @Test + public void testLcs() { + System.out.println("lcs"); + String s1 = "switzerland"; + String s2 = "disneyland"; + String expResult = "land"; + String result = Batch.suffix(s1, s2); + assertEquals(expResult, result); + s2 = "sweden"; + expResult = ""; + result = Batch.suffix(s1, s2); + assertEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/layout/BoundingBoxTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/layout/BoundingBoxTest.java new file mode 100644 index 00000000..3375a094 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/layout/BoundingBoxTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.layout; + +import java.awt.Polygon; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class BoundingBoxTest { + + /** + * Test of asPolygon method, of class BoundingBox. + */ + @Test + public void testToPolygon() { + System.out.println("toPolygon"); + Polygon expResult = new BoundingBox(0, 0, 20, 20).asPolygon(); + BoundingBox instance = new BoundingBox(0, 0, 10, 20); + instance.add(new BoundingBox(10, 10, 20, 20)); + + assertEquals(expResult.getBounds(), instance); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/layout/ComponentTagTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/layout/ComponentTagTest.java new file mode 100644 index 00000000..f05e330a --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/layout/ComponentTagTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package eu.digitisation.layout; + +import eu.digitisation.input.FileType; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class ComponentTagTest { + + /** + * Test of shortTag method, of class ComponentTag. + */ + @Test + public void testShortTag() { + System.out.println("shortTag"); + ComponentTag tag = ComponentTag.valueOf(FileType.PAGE, "TextRegion"); + String expResult = "TextRegion"; + String result = ComponentTag.shortTag(tag); + assertEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/math/ArraysTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/math/ArraysTest.java new file mode 100644 index 00000000..efdc2b40 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/math/ArraysTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class ArraysTest { + + /** + * Test of sum method, of class ArrayMath. + */ + @Test + public void testSum_intArr() { + System.out.println("sum"); + int[] array = {1, 2, 3, 0, -1}; + int expResult = 5; + int result = Arrays.sum(array); + assertEquals(expResult, result); + } + + /** + * Test of sum method, of class ArrayMath. + */ + @Test + public void testSum_doubleArr() { + System.out.println("sum"); + double[] array = {1, 2, 3, 0, -1}; + double expResult = 5; + double result = Arrays.sum(array); + assertEquals(expResult, result, 0.01); + } + + /** + * Test of average method, of class ArrayMath. + */ + @Test + public void testAverage_intArr() { + System.out.println("average"); + int[] array = {1, 2, 3, -2}; + double expResult = 1.0; + double result = Arrays.average(array); + assertEquals(expResult, result, 0.0001); + } + + /** + * Test of average method, of class ArrayMath. + */ + @Test + public void testAverage_doubleArr() { + System.out.println("average"); + double[] array = {1, 2, 3, -2}; + double expResult = 1.0; + double result = Arrays.average(array); + assertEquals(expResult, result, 0.0001); + } + + /** + * Test of logaverage method, of class ArrayMath. + */ + @Test + public void testLogaverage_intArr() { + System.out.println("logaverage"); + int[] array = {10, 100, 1000}; + double expResult = 100.0; + double result = Arrays.logaverage(array); + assertEquals(expResult, result, 0.001); + } + + /** + * Test of logaverage method, of class ArrayMath. + */ + @Test + public void testLogaverage_doubleArr() { + System.out.println("logaverage"); + double[] array = {10, 100, 1000}; + double expResult = 100.0; + double result = Arrays.logaverage(array); + assertEquals(expResult, result, 0.001); + } + + /** + * Test of scalar method, of class ArrayMath. + */ + @Test + public void testScalar() { + System.out.println("scalar"); + double[] x = {1, 2, 3}; + double[] y = {1, 2, 3}; + double expResult = 14.0; + double result = Arrays.scalar(x, y); + assertEquals(expResult, result, 0.0001); + } + + /** + * Test of max method, of class ArrayMath. + */ + @Test + public void testMax_intArr() { + System.out.println("max"); + int[] array = {-5, 2, 3}; + int expResult = 3; + int result = Arrays.max(array); + assertEquals(expResult, result); + } + + /** + * Test of max method, of class ArrayMath. + */ + @Test + public void testMax_doubleArr() { + System.out.println("max"); + double[] array = {-5, 2, 3}; + double expResult = 3; + double result = Arrays.max(array); + assertEquals(expResult, result, 0.0); + } + + /** + * Test of min method, of class ArrayMath. + */ + @Test + public void testMin_intArr() { + System.out.println("min"); + int[] array = {2, -1}; + int expResult = -1; + int result = Arrays.min(array); + assertEquals(expResult, result); + } + + /** + * Test of min method, of class ArrayMath. + */ + @Test + public void testMin_doubleArr() { + System.out.println("min"); + double[] array = {2, 0, -1}; + double expResult = -1.0; + double result = Arrays.min(array); + assertEquals(expResult, result, 0.0001); + } + + /** + * Test of cov method, of class ArrayMath. + */ + @Test + public void testCov_intArr_intArr() { + System.out.println("cov"); + int[] X = {1, 2, 3}; + int[] Y = {1, 2, 3}; + double expResult = 2.0/3; + double result = Arrays.cov(X, Y); + assertEquals(expResult, result, 0.001); + } + + /** + * Test of cov method, of class ArrayMath. + */ + @Test + public void testCov_doubleArr_doubleArr() { + System.out.println("cov"); + double[] X = {1, 2, 3}; + double[] Y = {1, 2, 3}; + double expResult = 2.0/3; + double result = Arrays.cov(X, Y); + assertEquals(expResult, result, 0.001); + } + + /** + * Test of std method, of class ArrayMath. + */ + @Test + public void testStd_intArr() { + System.out.println("std"); + int[] X = {1, 2, 2, 3}; + double expResult = Math.sqrt(0.5); + double result = Arrays.std(X); + assertEquals(expResult, result, 0.0001); + } + + /** + * Test of std method, of class ArrayMath. + */ + @Test + public void testStd_doubleArr() { + System.out.println("std"); + double[] X = {1, 2, 2, 3}; + double expResult = Math.sqrt(0.5); + double result = Arrays.std(X); + assertEquals(expResult, result, 0.0001); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/math/BiCounterTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/math/BiCounterTest.java new file mode 100644 index 00000000..2cbd358c --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/math/BiCounterTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author R.C.C + */ +public class BiCounterTest { + + public BiCounterTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of value method, of class BiCounter. + */ + @Test + public void testValue() { + System.out.println("value"); + Object o1 = null; + Object o2 = null; + BiCounter bc = new BiCounter(); + bc.inc(1, 2); + bc.inc(1, 3); + bc.add(1, 3, 4); + assertEquals(1, bc.value(1, 2)); + assertEquals(5, bc.value(1, 3)); + assertEquals(6, bc.value(1, null)); + assertEquals(6, bc.total()); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/math/CounterTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/math/CounterTest.java new file mode 100644 index 00000000..6c0e9232 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/math/CounterTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import java.util.List; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author R.C.C + */ +public class CounterTest { + + public CounterTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of add method, of class Counter. + */ + @Test + public void test() { + System.out.println("add"); + Object object = null; + int value = 0; + Counter instance = new Counter(); + instance.add(1, 3); + instance.inc(1); + instance.add(1, -1); + assertEquals(instance.get(1).intValue(), 3); + } + + @Test + public void testKeyList() { + System.out.println("keyList"); + Counter instance = new Counter(); + instance.add(1, 6); + instance.add(2, 3); + instance.add(3, 1); + instance.add(4, 5); + Integer[] expResult = {3, 2, 4, 1}; + Integer[] result = new Integer[4]; + List list = instance.keyList(Counter.Order.ASCENDING_VALUE); + System.out.println(list); + list.toArray(result); + assertArrayEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/math/PairTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/math/PairTest.java new file mode 100644 index 00000000..6b0eefef --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/math/PairTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.math; + +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class PairTest { + /** + * Test of equals method, of class Pair. + */ + @Test + public void testEquals() { + System.out.println("equals"); + Object o = null; + Pair p1 = new Pair("a", "b"); + Pair p2 = new Pair("a", "b"); + Pair p3 = new Pair("a", "c"); + assert (p1.equals(p2)); + assert (!p1.equals(p3)); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/ngram/NgramModelTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/ngram/NgramModelTest.java new file mode 100644 index 00000000..988f91d9 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/ngram/NgramModelTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.ngram; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author carrasco@ua.es + */ +public class NgramModelTest { + + @Test + public void testSize() { + System.out.println("size"); + NgramModel ngrams = new NgramModel(2); + ngrams.addWord("0000"); + ngrams.addWord("0100"); + + int expResult = 9; // 5 bi-grams plus 3 uni-grams plus 1 0-gram + int result = ngrams.size(); + + assertEquals(expResult, result); + + ngrams = new NgramModel(3); + ngrams.addWord("0000"); + ngrams.addWord("0100"); + + expResult = 15; // 6 tri-grams + 5 bi-grams +3 uni-grams + 1 0-gram + result = ngrams.size(); + + assertEquals(expResult, result); + + } + + @Test + public void testGetGoodTuringPars() { + System.out.println("size"); + NgramModel ngrams = new NgramModel(3); + ngrams.addWord("0000"); + ngrams.addWord("0100"); + double[] expResult = {0.1, 0.2, 0.5}; + double[] result = ngrams.getGoodTuringPars(); + assertEquals(expResult.length, result.length); + for (int n = 0; n < result.length; ++n) { + assertEquals(expResult[n], result[n], 0.001); + } + + } + + @Test + public void testProb() { + System.out.println("prob"); + NgramModel ngrams = new NgramModel(3); + ngrams.addWord("0000"); + ngrams.addWord("0100"); + + assertEquals(4 / (double) 7, ngrams.prob("00"), 0.001); + assertEquals(0.7, ngrams.prob("0"), 0.001); + } + + @Test + public void testSmoothProb() { + System.out.println("prob"); + NgramModel ngrams = new NgramModel(3); + ngrams.addWord("0000"); + ngrams.addWord("0100"); + + double expResult = 0.8 * (4 / (double) 7) + 0.2 * 0.7; + double result = ngrams.smoothProb("00"); + assertEquals(expResult, result, 0.001); + + expResult = 0.8 * (2 / (double) 7) + 0.2 * 0.2; + result = ngrams.smoothProb("0" + ngrams.EOS); + assertEquals(expResult, result, 0.001); + } + + @Test + public void testWordLogProb() { + System.out.println("wordLogProb"); + NgramModel instance = new NgramModel(1); + instance.addWord("lava"); + double expResult = (3 * Math.log(0.2) + 2 * Math.log(0.4)); + double result = instance.logWordProb("lava"); + assertEquals(expResult, result, 0.01); + } + + @Test + public void testLogProb() { + System.out.println("logProb"); + NgramModel instance = new NgramModel(1); + instance.addWord("lava"); + double expResult = -Math.log(5); + double result = instance.logProb("baba", 'v'); + assertEquals(expResult, result, 0.01); + + instance = new NgramModel(2); + instance.addWord("lava"); + expResult = Math.log(0.2); + result = instance.logProb("ca", 'v'); + assertEquals(expResult, result, 0.01); + } + + @Test + public void testAddSubstrings() { + System.out.println("addSubstrings"); + String EOS = String.valueOf(NgramModel.EOS); + NgramModel ngrams = new NgramModel(3); + NgramModel ref = new NgramModel(3); + + ngrams.addSubstrings("b", "cde"); + + // 3-grams +// ref.addEntry("abc"); + ref.addEntry("bcd"); + ref.addEntry("cde"); + + // 2-grams + ref.addEntry("bc"); + ref.addEntry("cd"); + ref.addEntry("de"); + + // 1-grams + ref.addEntry("c"); + ref.addEntry("d"); + ref.addEntry("e"); + + // 0-grams + ref.addEntries("", 3); + ref.showDiff(ngrams); + + assertEquals(ref, ngrams); + + } + + @Test + public void testAddText() { + System.out.println("addText"); + String BOS = String.valueOf(NgramModel.BOS); + String EOS = String.valueOf(NgramModel.EOS); + NgramModel ngrams = new NgramModel(3); + NgramModel ref = new NgramModel(3); + String input = "ab\nc"; + InputStream is = new ByteArrayInputStream(input.getBytes()); + + // result + ngrams.addText(is); + + // expected result + // 3-grams + ref.addEntry(BOS + "ab"); + ref.addEntry("ab "); + ref.addEntry("b c"); + ref.addEntry(" c" + EOS); + + //2-grams + ref.addEntry(BOS + 'a'); + ref.addEntry("ab"); + ref.addEntry("b "); + ref.addEntry(" c"); + ref.addEntry("c" + EOS); + // 1-grams + ref.addEntry("a"); + ref.addEntry("b"); + ref.addEntry(" "); + ref.addEntry("c"); + ref.addEntry(EOS); + // 0-grams + ref.addEntries("", 5); + + ref.showDiff(ngrams); + + assertEquals(ref, ngrams); + + String text = "ab"; + + is = new ByteArrayInputStream(text.getBytes()); + double expectedResult = Math.log(0.2); + double result = ngrams.logLikelihood(is, 0); + assertEquals(expectedResult, result, 0.0001); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/text/CharFilterTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/text/CharFilterTest.java new file mode 100644 index 00000000..abe94bd0 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/text/CharFilterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.text.CharFilter; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class CharFilterTest { + + public CharFilterTest() { + } + + + /** + * Test of translate method, of class CharFilter. + * + * @throws java.net.URISyntaxException + */ + @Test + public void testTranslate_String() throws URISyntaxException { + System.out.println("translate"); + URL resourceUrl = getClass().getResource("/UnicodeCharEquivalences.txt"); + File file = new File(resourceUrl.toURI()); + CharFilter filter = new CharFilter(file); + String s = "a\u0133"; // ij + String expResult = "aij"; + String result = filter.translate(s); + assertEquals(expResult.length(), result.length()); + assertEquals(expResult, result); + } + + @Test + public void testCompatibilityMode() { + System.out.println("compatibility"); + CharFilter filter = new CharFilter(); + String s = "\u0133"; + String r = "ij"; + assert (!r.equals(filter.translate(s))); + filter.setCompatibility(true); + assertEquals(r, filter.translate(s)); + + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/text/CharMapTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/text/CharMapTest.java new file mode 100644 index 00000000..d9dac59c --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/text/CharMapTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 U. de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import eu.digitisation.text.CharMap; +import eu.digitisation.text.CharMap.Option; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class CharMapTest { + + public CharMapTest() { + } + + /** + * Test of normalForm method, of class CharMap. + * @throws java.net.URISyntaxException + */ + @Test + public void testNormalForm_String() throws URISyntaxException { + System.out.println("normalForm"); + String s = "Mañana! Antígona2"; + Option[] ops = {}; + CharMap map = new CharMap(ops); + String expResult = "manana antigona2"; + String result = map.normalForm(s); + assertEquals(expResult, result); + // Test comaptiblity file + URL resourceUrl = getClass().getResource("/UnicodeCharEquivalences.txt"); + File file = new File(resourceUrl.toURI()); + CharMap filter = new CharMap(); + filter.addFilter(file); + s = "a\uf50d"; // triple ligature not in Unicode compatibilty + expResult = "aq\u0301\uA76B"; // q + acute + et + result = filter.normalForm(s); + assertEquals(expResult, result); + } + + /** + * Test of normalForm method, of class CharMap. + */ + @Test + public void testNormalForm_char() { + System.out.println("normalForm"); + char longs = '\u017F'; // a long s + char ff = '\ufb00'; + Option[] ops = {}; + CharMap map = new CharMap(ops); + String expResult; + String result; + result = map.normalForm(longs); + expResult = "s"; + assertEquals(expResult, result); + result = map.normalForm(ff); + expResult = "ff"; + assertEquals(expResult, result); + } + + /** + * Test of equiv method, of class CharMap. + */ + @Test + public void testEquiv() { + System.out.println("equiv"); + char c1 = '?'; + char c2 = ' '; + Option[] ops = {}; + CharMap instance = new CharMap(ops); + boolean expResult = true; + boolean result = instance.equiv(c1, c2); + assertEquals(expResult, result); + } + +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/text/StringNormalizerTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/text/StringNormalizerTest.java new file mode 100644 index 00000000..96f58927 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/text/StringNormalizerTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author rafa + */ +public class StringNormalizerTest { + + public StringNormalizerTest() { + } + + /** + * Test of reduceWS method, of class StringNormalizer. + */ + @Test + public void testReduceWS() { + System.out.println("reduceWS"); + String s = "one \rtwo\nthree"; + String expResult = "one two three"; + String result = StringNormalizer.reduceWS(s); + assertEquals(expResult, result); + } + + /** + * Test of composed method, of class StringNormalizer. + */ + @Test + public void testComposed() { + System.out.println("composed"); + String s = "n\u0303"; + String expResult = "ñ"; + String result = StringNormalizer.composed(s); + assertEquals(expResult, result); + } + + /** + * Test of compatible method, of class StringNormalizer. + */ + @Test + public void testCompatible() { + System.out.println("compatible"); + String s = "\ufb00"; // ff ligature + String expResult = "ff"; + String result = StringNormalizer.compatible(s); + assertEquals(expResult, result); + } + + /** + * Test of removeDiacritics method, of class StringNormalizer. + */ + @Test + public void testRemoveDiacritics() { + System.out.println("removeDiacritics"); + String s = "cañón"; + String expResult = "canon"; + String result = StringNormalizer.removeDiacritics(s); + assertEquals(expResult, result); + } + + /** + * Test of removePunctuation method, of class StringNormalizer. + */ + @Test + public void testRemovePunctuation() { + System.out.println("removePunctuation"); + String s = "!\"#}-"; // + is not in punctuation block + String expResult = ""; + String result = StringNormalizer.removePunctuation(s); + assertEquals(expResult, result); + } + + @Test + public void testTrim() { + System.out.println("trim"); + String s = "! \"#lin?ks+!\"#}-"; // + is not in punctuation block + String expResult = "lin?ks+"; + String result = StringNormalizer.trim(s); + assertEquals(expResult, result); + } + + @Test + public void testStrip() { + System.out.println("strip"); + String s = "Stra\u00dfe+ links+!\"#}-"; // ª is a letter! + String expResult = "Stra\u00dfe links"; + String result = StringNormalizer.strip(s); + assertEquals(expResult, result); + } + + /** + * Test of encode method, of class StringNormalizer. + */ + @Test + public void testEncode() { + System.out.println("encode"); + String s = "<\">"; + String expResult = "<">"; + String result = StringNormalizer.encode(s); + assertEquals(expResult, result); + + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/text/TestUnicodeReader.java b/ocrevalUAtion/src/test/java/eu/digitisation/text/TestUnicodeReader.java new file mode 100644 index 00000000..f45ef4f7 --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/text/TestUnicodeReader.java @@ -0,0 +1,20 @@ +package eu.digitisation.text; + +import eu.digitisation.text.UnicodeReader; +import junit.framework.TestCase; +/** + * + * @author R.C.C + */ +public class TestUnicodeReader extends TestCase { + + public void testUnicodeReader() { + String input = "día, mes y año"; + String ref = "[100, 237, 97, 44, 32, 109, 101, 115, 32, 121, 32, 97, 241, 111]"; + + String output = + java.util.Arrays.toString(UnicodeReader.toCodepoints(input)); + //System.out.println(output); + assertEquals(ref, output); + } +} diff --git a/ocrevalUAtion/src/test/java/eu/digitisation/text/WordScannerTest.java b/ocrevalUAtion/src/test/java/eu/digitisation/text/WordScannerTest.java new file mode 100644 index 00000000..c3aa589a --- /dev/null +++ b/ocrevalUAtion/src/test/java/eu/digitisation/text/WordScannerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 Universidad de Alicante + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package eu.digitisation.text; + +import java.io.IOException; +import static junit.framework.TestCase.assertEquals; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * + * @author R.C.C + */ +public class WordScannerTest { + + public WordScannerTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of main method, of class WordScanner. + * + * @throws java.io.IOException + */ + @Test + public void testnextWord() throws IOException { + System.out.println("main"); + String input = "hola&amigo2\n3.14 mi casa, todos los días\n" + + "mesa-camilla java4you i.b.m. i+d Dª María 3+100%"; + WordScanner scanner = new WordScanner(input, null); + String word; + int num = 0; + while ((word = scanner.nextWord()) != null) { + ++num; + //System.out.println(word); + } + assertEquals(18, num); + + } +} diff --git a/ocrevalUAtion/userProperties.xml b/ocrevalUAtion/userProperties.xml new file mode 100644 index 00000000..0cb7bd58 --- /dev/null +++ b/ocrevalUAtion/userProperties.xml @@ -0,0 +1,9 @@ + + + + + + 100000 + + \ No newline at end of file diff --git a/ocrevalUAtion/userProperties_test.xml b/ocrevalUAtion/userProperties_test.xml new file mode 100644 index 00000000..8e486fd0 --- /dev/null +++ b/ocrevalUAtion/userProperties_test.xml @@ -0,0 +1,13 @@ + + + + + http://bibnum.bnf.fr/ns/alto_prod + http://bibnum.bnf.fr/ns/alto_prod.xsd + + + + 0 + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..3045b965 --- /dev/null +++ b/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + de.vorb.tesseract + tesseract4java + 0.3.0-SNAPSHOT + pom + + + gui + tools + ocrevalUAtion + + + + UTF-8 + 1.8 + 1.8 + + + + + + de.vorb.tesseract + tools + ${project.version} + + + de.vorb.tesseract + ocrevalUAtion + ${project.version} + + + + + + + + com.amashchenko.maven.plugin + gitflow-maven-plugin + 1.12.0 + + 1 + true + true + false + false + + + + + + diff --git a/screenshots/gui-batch-export.png b/screenshots/gui-batch-export.png new file mode 100644 index 00000000..70bee5ed Binary files /dev/null and b/screenshots/gui-batch-export.png differ diff --git a/screenshots/gui-box-editor.png b/screenshots/gui-box-editor.png new file mode 100644 index 00000000..e94c4c5c Binary files /dev/null and b/screenshots/gui-box-editor.png differ diff --git a/screenshots/gui-comparison.png b/screenshots/gui-comparison.png new file mode 100644 index 00000000..e64cc54b Binary files /dev/null and b/screenshots/gui-comparison.png differ diff --git a/screenshots/gui-evaluation.png b/screenshots/gui-evaluation.png new file mode 100644 index 00000000..e83ea2ef Binary files /dev/null and b/screenshots/gui-evaluation.png differ diff --git a/screenshots/gui-glyph-overview.png b/screenshots/gui-glyph-overview.png new file mode 100644 index 00000000..bbe069dc Binary files /dev/null and b/screenshots/gui-glyph-overview.png differ diff --git a/screenshots/gui-preprocessing.png b/screenshots/gui-preprocessing.png new file mode 100644 index 00000000..ab71257a Binary files /dev/null and b/screenshots/gui-preprocessing.png differ diff --git a/screenshots/ocrevaluation.png b/screenshots/ocrevaluation.png new file mode 100644 index 00000000..f959768a Binary files /dev/null and b/screenshots/ocrevaluation.png differ diff --git a/src/main/java/de/vorb/tesseract/gui/controller/PageLoader.java b/src/main/java/de/vorb/tesseract/gui/controller/PageLoader.java deleted file mode 100644 index 067b16d9..00000000 --- a/src/main/java/de/vorb/tesseract/gui/controller/PageLoader.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.vorb.tesseract.gui.controller; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.nio.file.Path; - -import org.bridj.Pointer; - -import de.vorb.leptonica.LibLept; -import de.vorb.leptonica.Pix; -import de.vorb.leptonica.util.PixConversions; -import de.vorb.tesseract.LibTess; -import de.vorb.tesseract.OCREngineMode; -import de.vorb.tesseract.PageSegMode; -import de.vorb.tesseract.tools.recognition.Recognition; - -public class PageLoader extends Recognition { - private BufferedImage originalImg = null; - private Pointer originalRef = null; - - public PageLoader(String language) throws IOException { - super(language); - } - - @Override - protected void init() throws IOException { - setHandle(LibTess.TessBaseAPICreate()); - } - - @Override - protected void reset() throws IOException { - // init LibTess with data path, language and OCR engine mode - LibTess.TessBaseAPIInit2( - getHandle(), - Pointer.pointerToCString("E:\\Masterarbeit\\Ressourcen\\tessdata"), - Pointer.pointerToCString(getLanguage()), OCREngineMode.DEFAULT); - - // set page segmentation mode - LibTess.TessBaseAPISetPageSegMode(getHandle(), PageSegMode.AUTO); - } - - @Override - protected void close() throws IOException { - LibTess.TessBaseAPIDelete(getHandle()); - } - - public void setOriginalImage(Pointer pix) { - LibTess.TessBaseAPISetImage2(getHandle(), pix); - } - - public BufferedImage getOriginalImage() { - return originalImg; - } - - public BufferedImage getThresholdedImage() { - if (originalImg.getType() == BufferedImage.TYPE_BYTE_BINARY) { - return originalImg; - } - - final Pointer img = LibTess.TessBaseAPIGetThresholdedImage(getHandle()); - - return PixConversions.pix2img(img); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java b/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java deleted file mode 100644 index 2a950495..00000000 --- a/src/main/java/de/vorb/tesseract/gui/controller/TesseractController.java +++ /dev/null @@ -1,217 +0,0 @@ -package de.vorb.tesseract.gui.controller; - -import java.awt.Cursor; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; - -import javax.imageio.ImageIO; -import javax.swing.DefaultListModel; -import javax.swing.ListSelectionModel; -import javax.swing.SwingWorker; -import javax.swing.UIManager; - -import org.bridj.BridJ; - -import de.vorb.tesseract.PageIteratorLevel; -import de.vorb.tesseract.gui.event.PageChangeListener; -import de.vorb.tesseract.gui.event.ProjectChangeListener; -import de.vorb.tesseract.gui.model.FilteredListModel; -import de.vorb.tesseract.gui.model.PageModel; -import de.vorb.tesseract.gui.view.TesseractFrame; -import de.vorb.tesseract.tools.recognition.DefaultRecognitionConsumer; -import de.vorb.tesseract.tools.recognition.RecognitionState; -import de.vorb.tesseract.util.Box; -import de.vorb.tesseract.util.TrainingFiles; -import de.vorb.tesseract.util.Line; -import de.vorb.tesseract.util.Page; -import de.vorb.tesseract.util.Project; -import de.vorb.tesseract.util.Symbol; -import de.vorb.tesseract.util.Word; - -public class TesseractController implements ProjectChangeListener, - PageChangeListener { - - private final TesseractFrame view; - private SwingWorker pageLoaderWorker = null; - private PageLoader pageLoader = null; - - // Filter for image files - private static final DirectoryStream.Filter IMG_FILTER = - new DirectoryStream.Filter() { - @Override - public boolean accept(Path entry) throws IOException { - return entry.toString().endsWith(".png") - || entry.toString().endsWith(".tif") - || entry.toString().endsWith(".tiff") - || entry.toString().endsWith(".jpg") - || entry.toString().endsWith(".jpeg"); - } - }; - - public static void main(String[] args) { - BridJ.setNativeLibraryFile("leptonica", new File("liblept170.dll")); - BridJ.setNativeLibraryFile("tesseract", new File("libtesseract303.dll")); - - final TesseractController controller = new TesseractController(); - } - - public TesseractController() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - // fail silently - // If the system LaF is not available, use whatever LaF is already - // being used. - } - - // create new tesseract frame - view = new TesseractFrame(); - - try { - pageLoader = new PageLoader("deu-frak"); - - // init training files - final List trainingFiles = TrainingFiles.getAvailable(); - - // prepare training file list model - final DefaultListModel trainingFilesModel = - new DefaultListModel<>(); - for (String trainingFile : trainingFiles) { - trainingFilesModel.addElement(trainingFile); - } - - // wrap it in a filtered model - view.getTrainingFiles().getList().setSelectionMode( - ListSelectionModel.SINGLE_SELECTION); - view.getTrainingFiles().getList().setModel( - new FilteredListModel(trainingFilesModel)); - } catch (Exception e) { - e.printStackTrace(); - } - - // register listeners - view.getLoadProjectDialog().addProjectChangeListener(this); - - view.setVisible(true); - } - - @Override - public void projectChanged(Path scanDir) { - try { - final ArrayList pages = new ArrayList<>(); - - final Iterator dirIt = Files.newDirectoryStream(scanDir, - IMG_FILTER).iterator(); - - while (dirIt.hasNext()) { - final Path file = dirIt.next(); - - if (Files.isDirectory(file)) - continue; - - pages.add(file); - } - - final Project project = new Project(scanDir, pages); - project.addPageChangeListener(this); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void pageSelectionChanged(int pageIndex) { - view.getPageLoadProgressBar().setIndeterminate(true); - view.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - - /* - * final Path page = - * view.getPageSelectionPane().getModel().getSelectedPage(); - * - * pageLoaderWorker = new SwingWorker() { - * - * @Override protected PageModel doInBackground() { try { return - * loadPageModel(page); } catch (IOException e) { e.printStackTrace(); - * return null; } } - * - * @Override public void done() { try { final PageModel page = get(); - * view.setModel(page); - * view.getPageLoadProgressBar().setIndeterminate(false); - * view.setCursor(Cursor.getDefaultCursor()); } catch - * (InterruptedException e) { e.printStackTrace(); } catch - * (ExecutionException e) { e.printStackTrace(); } } }; - * - * pageLoaderWorker.execute(); - */ - } - - private PageModel loadPageModel(Path scanFile) throws IOException { - pageLoader.reset(); - - final Vector lines = new Vector(); - - // Get images - final BufferedImage originalImg = ImageIO.read(scanFile.toFile()); - final BufferedImage thresholdedImg = pageLoader.getThresholdedImage(); - - pageLoader.recognize(new DefaultRecognitionConsumer() { - private ArrayList lineWords; - private ArrayList wordSymbols; - - @Override - public void lineBegin() { - lineWords = new ArrayList<>(); - } - - @Override - public void lineEnd() { - final PageIteratorLevel level = PageIteratorLevel.TEXTLINE; - lines.add(new Line(getState().getBoundingBox(level), lineWords, - getState().getBaseline(level))); - } - - @Override - public void wordBegin() { - wordSymbols = new ArrayList<>(); - } - - @Override - public void wordEnd() { - final RecognitionState state = getState(); - final PageIteratorLevel level = PageIteratorLevel.WORD; - final Box bbox = state.getBoundingBox(level); - lineWords.add(new Word(wordSymbols, bbox, - state.getConfidence(level), - state.getBaseline(PageIteratorLevel.WORD), - state.getWordFontAttributes())); - } - - @Override - public void symbol() { - final PageIteratorLevel level = PageIteratorLevel.SYMBOL; - wordSymbols.add(new Symbol(getState().getText(level), - getState().getBoundingBox(level), - getState().getConfidence(level))); - } - }); - - final Page page = new Page(scanFile, originalImg.getWidth(), - originalImg.getHeight(), 300, lines); - - // try { - // page.writeTo(System.out); - // } catch (JAXBException e) { - // e.printStackTrace(); - // } - - return null; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/event/LocaleChangeListener.java b/src/main/java/de/vorb/tesseract/gui/event/LocaleChangeListener.java deleted file mode 100644 index a92f46c2..00000000 --- a/src/main/java/de/vorb/tesseract/gui/event/LocaleChangeListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.vorb.tesseract.gui.event; - -public interface LocaleChangeListener { - public void localeChanged(); -} diff --git a/src/main/java/de/vorb/tesseract/gui/event/package-info.java b/src/main/java/de/vorb/tesseract/gui/event/package-info.java deleted file mode 100644 index e83d7319..00000000 --- a/src/main/java/de/vorb/tesseract/gui/event/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * - */ -/** - * @author Paul Vorbach - * - */ -package de.vorb.tesseract.gui.event; \ No newline at end of file diff --git a/src/main/java/de/vorb/tesseract/gui/model/GlobalPrefs.java b/src/main/java/de/vorb/tesseract/gui/model/GlobalPrefs.java deleted file mode 100644 index 505f0f0a..00000000 --- a/src/main/java/de/vorb/tesseract/gui/model/GlobalPrefs.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.vorb.tesseract.gui.model; - -import java.nio.file.Paths; -import java.util.prefs.Preferences; - -public final class GlobalPrefs { - public static final String TESSDATA_DIR = "path.tessdata"; - public static final String TESSDATA_DIR_DEFAULT = Paths.get( - System.getenv("TESSDATA_PREFIX")).resolve("tessdata").toString(); - - private static GlobalPrefs instance = null; - - private GlobalPrefs() { - } - - public static Preferences getPrefs() { - if (instance == null) { - instance = new GlobalPrefs(); - } - - return Preferences.userNodeForPackage(instance.getClass()); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/model/PageModel.java b/src/main/java/de/vorb/tesseract/gui/model/PageModel.java deleted file mode 100644 index d44c90fc..00000000 --- a/src/main/java/de/vorb/tesseract/gui/model/PageModel.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.vorb.tesseract.gui.model; - -import java.nio.file.Path; - -import de.vorb.tesseract.util.Page; - -public class PageModel { - private final Page page; - private final Path imageFile; - - public PageModel(Page page, Path imageFile) { - this.page = page; - this.imageFile = imageFile; - } - - public Page getPage() { - return page; - } - - public Path getImageFile() { - return imageFile; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java b/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java deleted file mode 100644 index ccf708ff..00000000 --- a/src/main/java/de/vorb/tesseract/gui/model/SymbolTableModel.java +++ /dev/null @@ -1,102 +0,0 @@ -package de.vorb.tesseract.gui.model; - -import java.util.Iterator; -import java.util.LinkedList; - -import javax.swing.table.AbstractTableModel; - -import de.vorb.tesseract.util.Symbol; - -public class SymbolTableModel extends AbstractTableModel { - private static final long serialVersionUID = 1L; - - private final LinkedList symbols; - - public SymbolTableModel() { - symbols = new LinkedList<>(); - } - - @Override - public int getColumnCount() { - return 6; - } - - @Override - public int getRowCount() { - return symbols.size(); - } - - @Override - public Object getValueAt(int rowIndex, int colIndex) { - switch (colIndex) { - case 0: - return rowIndex + 1; - case 1: - return symbols.get(rowIndex).getText(); - case 2: - return symbols.get(rowIndex).getBoundingBox().getX(); - case 3: - return symbols.get(rowIndex).getBoundingBox().getY(); - case 4: - return symbols.get(rowIndex).getBoundingBox().getWidth(); - case 5: - return symbols.get(rowIndex).getBoundingBox().getHeight(); - } - - return null; - } - - @Override - public String getColumnName(int colIndex) { - switch (colIndex) { - case 0: - return "No."; - case 1: - return "Symbol"; - case 2: - return "X"; - case 3: - return "Y"; - case 4: - return "Width"; - case 5: - return "Height"; - } - - return ""; - } - - @Override - public Class getColumnClass(int colIndex) { - switch (colIndex) { - case 0: - return Integer.class; - case 1: - return String.class; - case 2: - return Integer.class; - case 3: - return Integer.class; - case 4: - return Integer.class; - case 5: - return Integer.class; - } - - return Object.class; - } - - public Symbol getSymbol(int index) { - return symbols.get(index); - } - - public void replaceAllSymbols(Iterator newSymbols) { - symbols.clear(); - - while (newSymbols.hasNext()) { - symbols.add(newSymbols.next()); - } - - fireTableDataChanged(); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/model/package-info.java b/src/main/java/de/vorb/tesseract/gui/model/package-info.java deleted file mode 100644 index b5651dc2..00000000 --- a/src/main/java/de/vorb/tesseract/gui/model/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * - */ -/** - * @author Paul Vorbach - * - */ -package de.vorb.tesseract.gui.model; \ No newline at end of file diff --git a/src/main/java/de/vorb/tesseract/gui/util/Resources.java b/src/main/java/de/vorb/tesseract/gui/util/Resources.java deleted file mode 100644 index e3e8c874..00000000 --- a/src/main/java/de/vorb/tesseract/gui/util/Resources.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.vorb.tesseract.gui.util; - -import javax.swing.Icon; -import javax.swing.ImageIcon; - -public class Resources { - public static final Icon getIcon(String name) { - return new ImageIcon(Resources.class.getResource("/icons/" + name - + ".png")); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java b/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java deleted file mode 100644 index ab9276e4..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/BoxEditor.java +++ /dev/null @@ -1,399 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import static de.vorb.tesseract.gui.view.Coordinates.unscaled; -import static javax.swing.Box.createHorizontalStrut; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.InputMethodEvent; -import java.awt.event.InputMethodListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.Iterator; - -import javax.swing.JLabel; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.JSlider; -import javax.swing.JSpinner; -import javax.swing.JSplitPane; -import javax.swing.JTable; -import javax.swing.JTextField; -import javax.swing.ListSelectionModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.event.TableModelEvent; -import javax.swing.table.TableColumnModel; -import javax.swing.text.PlainDocument; - -import de.vorb.tesseract.gui.event.SelectionListener; -import de.vorb.tesseract.gui.model.PageModel; -import de.vorb.tesseract.gui.model.SingleSelectionModel; -import de.vorb.tesseract.gui.model.SymbolTableModel; -import de.vorb.tesseract.gui.view.renderer.BoxFileRenderer; -import de.vorb.tesseract.util.Box; -import de.vorb.tesseract.util.Iterators; -import de.vorb.tesseract.util.Point; -import de.vorb.tesseract.util.Symbol; - -public class BoxEditor extends JPanel implements MainComponent { - private static final long serialVersionUID = 1L; - - private PageModel model = null; - private final SingleSelectionModel selectionModel = - new SingleSelectionModel(); - - private final JTextField tfSymbol; - private final JTable tabSymbols; - private final JSpinner spinX; - private final JSpinner spinY; - private final JSpinner spinWidth; - private final JSpinner spinHeight; - private final JLabel lblImage; - - private static final Dimension DEFAULT_SPINNER_DIMENSION = - new Dimension(50, 20); - - private final BoxFileRenderer renderer; - - private final JPopupMenu contextMenu; - - // FIXME concurrency issues - private final PropertyChangeListener boxChangeListener = new PropertyChangeListener() { - @Override - public void propertyChange(final PropertyChangeEvent e) { - if (!e.getPropertyName().startsWith("SPIN")) { - return; - } - - // don't do anything if no symbol is selected - if (getCurrentSymbol() == null) { - return; - } - - final Object source = e.getSource(); - final Symbol currentSymbol = getCurrentSymbol(); - - // if the source is one of the JSpinners for x, y, width and - // height, update the bounding box - if (source instanceof JSpinner) { - // get coords - final int x = (int) spinX.getValue(); - final int y = (int) spinY.getValue(); - final int width = (int) spinWidth.getValue(); - final int height = (int) spinHeight.getValue(); - - // create new box - final Box newBBox = new Box(x, y, width, height); - - // replace current box with new one - currentSymbol.setBoundingBox(newBBox); - - // re-render the whole model -// renderer.render(getModel().getPage(), -// getModel().getImageFile(), 1f); - } - - tabSymbols.tableChanged(new TableModelEvent(tabSymbols.getModel(), - tabSymbols.getSelectedRow())); - } - }; - - /** - * Create the panel. - */ - public BoxEditor() { - setLayout(new BorderLayout(0, 0)); - - // create table first, so it can be used by the property change listener - tabSymbols = new JTable(); - tabSymbols.setFillsViewportHeight(true); - tabSymbols.setModel(new SymbolTableModel()); - - { - // set column widths - final TableColumnModel colModel = tabSymbols.getColumnModel(); - colModel.getColumn(0).setPreferredWidth(30); - colModel.getColumn(0).setMaxWidth(40); - colModel.getColumn(1).setPreferredWidth(50); - colModel.getColumn(1).setMaxWidth(70); - colModel.getColumn(2).setPreferredWidth(40); - colModel.getColumn(2).setMaxWidth(60); - colModel.getColumn(3).setPreferredWidth(40); - colModel.getColumn(3).setMaxWidth(60); - colModel.getColumn(4).setPreferredWidth(40); - colModel.getColumn(4).setMaxWidth(60); - colModel.getColumn(5).setPreferredWidth(40); - colModel.getColumn(5).setMaxWidth(60); - } - - tabSymbols.getSelectionModel().setSelectionMode( - ListSelectionModel.SINGLE_SELECTION); - - tabSymbols.getSelectionModel().addListSelectionListener( - new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - selectionModel.setSelectedIndex(tabSymbols.getSelectedRow()); - -// renderer.render(model.getPage(), model.getImageFile(), 1f); - } - }); - - JPanel toolbar = new JPanel(); - add(toolbar, BorderLayout.NORTH); - - JSplitPane spMain = new JSplitPane(); - add(spMain, BorderLayout.CENTER); - toolbar.setLayout(new BorderLayout(0, 0)); - - JPanel panel_1 = new JPanel(); - toolbar.add(panel_1, BorderLayout.WEST); - - JLabel lblExample = new JLabel("Symbol"); - panel_1.add(lblExample); - - tfSymbol = new JTextField(); - - // listen for document changes - tfSymbol.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void removeUpdate(DocumentEvent e) { - change(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - change(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - change(); - } - - private void change() { - final Symbol current = getCurrentSymbol(); - if (current == null) { - return; - } - - current.setText(tfSymbol.getText()); - tabSymbols.tableChanged(new TableModelEvent( - tabSymbols.getModel(), tabSymbols.getSelectedRow(), 1)); - } - }); - panel_1.add(tfSymbol); - tfSymbol.setColumns(5); - - Component horizontalStrut = createHorizontalStrut(10); - panel_1.add(horizontalStrut); - - JLabel lblLeft = new JLabel("X"); - panel_1.add(lblLeft); - - spinX = new JSpinner(); - spinX.addPropertyChangeListener(boxChangeListener); - panel_1.add(spinX); - spinX.setPreferredSize(DEFAULT_SPINNER_DIMENSION); - spinX.setModel(new SpinnerNumberModel(0, 0, null, 1)); - - Component horizontalStrut_1 = createHorizontalStrut(5); - panel_1.add(horizontalStrut_1); - - JLabel lblTop = new JLabel("Y"); - panel_1.add(lblTop); - - spinY = new JSpinner(); - spinY.addPropertyChangeListener(boxChangeListener); - panel_1.add(spinY); - spinY.setPreferredSize(DEFAULT_SPINNER_DIMENSION); - spinY.setModel(new SpinnerNumberModel(0, 0, null, 1)); - - Component horizontalStrut_2 = createHorizontalStrut(5); - panel_1.add(horizontalStrut_2); - - JLabel lblRight = new JLabel("Width"); - panel_1.add(lblRight); - - spinWidth = new JSpinner(); - spinWidth.addPropertyChangeListener(boxChangeListener); - panel_1.add(spinWidth); - spinWidth.setPreferredSize(DEFAULT_SPINNER_DIMENSION); - spinWidth.setModel(new SpinnerNumberModel(0, 0, null, 1)); - - Component horizontalStrut_3 = createHorizontalStrut(5); - panel_1.add(horizontalStrut_3); - - JLabel lblBottom = new JLabel("Height"); - panel_1.add(lblBottom); - - spinHeight = new JSpinner(); - spinHeight.addPropertyChangeListener(boxChangeListener); - panel_1.add(spinHeight); - spinHeight.setPreferredSize(DEFAULT_SPINNER_DIMENSION); - spinHeight.setModel(new SpinnerNumberModel(0, 0, null, 1)); - - JPanel panel = new JPanel(); - toolbar.add(panel, BorderLayout.EAST); - - JLabel lblZoom = new JLabel("Zoom"); - panel.add(lblZoom); - - JSlider zoomSlider = new JSlider(); - zoomSlider.setMinorTickSpacing(1); - zoomSlider.setPreferredSize(new Dimension(160, 20)); - zoomSlider.setValue(5); - zoomSlider.setMajorTickSpacing(1); - zoomSlider.setMaximum(9); - zoomSlider.setMinimum(1); - panel.add(zoomSlider); - - JPanel sidebar = new JPanel(); - spMain.setLeftComponent(sidebar); - sidebar.setLayout(new BorderLayout(0, 0)); - - JScrollPane scrollPane_1 = new JScrollPane(); - sidebar.add(scrollPane_1, BorderLayout.CENTER); - - scrollPane_1.setViewportView(tabSymbols); - - scrollPane_1.setMinimumSize(new Dimension(200, 100)); - scrollPane_1.setPreferredSize(new Dimension(260, 10000)); - scrollPane_1.setMaximumSize(new Dimension(310, 10000)); - - JScrollPane scrollPane = new JScrollPane(); - spMain.setRightComponent(scrollPane); - - lblImage = new JLabel(""); - scrollPane.setViewportView(lblImage); - - contextMenu = new JPopupMenu("Box operations"); - contextMenu.add(new JMenuItem("Split box")); - contextMenu.add(new JSeparator()); - contextMenu.add(new JMenuItem("Merge with previous box")); - contextMenu.add(new JMenuItem("Merge with next box")); - - lblImage.addMouseListener(new MouseAdapter() { - public void mousePressed(MouseEvent e) { - clicked(e); - } - - public void mouseReleased(MouseEvent e) { - clicked(e); - } - - private void clicked(MouseEvent e) { - final float scale = 1f; - - final Point p = new Point(unscaled(e.getX(), scale), unscaled( - e.getY(), scale)); - - final Iterator it = - Iterators.symbolIterator(getModel().getPage()); - - final ListSelectionModel sel = tabSymbols.getSelectionModel(); - - boolean selection = false; - for (int i = 0; it.hasNext(); i++) { - final Box bbox = it.next().getBoundingBox(); - - if (bbox.contains(p)) { - selection = true; - selectionModel.setSelectedIndex(i); - sel.setSelectionInterval(i, i); - break; - } - } - - if (!selection) { - selectionModel.setSelectedIndex(-1); - sel.setSelectionInterval(-1, -1); - } else if (e.isPopupTrigger()) { - contextMenu.show(e.getComponent(), e.getX(), e.getY()); - } - -// renderer.render(model.getPage(), model.getBlackAndWhiteImage(), -// scale); - } - }); - - selectionModel.addSelectionListener(new SelectionListener() { - @Override - public void selectionChanged(int index) { - if (index < 0) { - return; - } - - final SymbolTableModel tabModel = - (SymbolTableModel) tabSymbols.getModel(); - final Symbol symbol = tabModel.getSymbol(index); - - final String symbolText = symbol.getText(); - tfSymbol.setText(symbolText); - - // tooltip with codepoints - final StringBuilder tooltip = new StringBuilder("[ "); - for (int i = 0; i < symbolText.length(); i++) { - tooltip.append( - Integer.toHexString(symbolText.codePointAt(i)) - ).append(' '); - } - tfSymbol.setToolTipText(tooltip.append(']').toString()); - - final Box bbox = symbol.getBoundingBox(); - spinX.setValue(bbox.getX()); - spinY.setValue(bbox.getY()); - spinWidth.setValue(bbox.getWidth()); - spinHeight.setValue(bbox.getHeight()); - } - }); - - renderer = new BoxFileRenderer(lblImage, selectionModel); - } - - @Override - public void setModel(PageModel model) { -// renderer.render(model.getPage(), model.getBlackAndWhiteImage(), 1f); - - final SymbolTableModel tabModel = - (SymbolTableModel) tabSymbols.getModel(); - - tabModel.replaceAllSymbols(Iterators.symbolIterator(model.getPage())); - - this.model = model; - } - - @Override - public PageModel getModel() { - return model; - } - - @Override - public Component asComponent() { - return this; - } - - private Symbol getCurrentSymbol() { - final int index = tabSymbols.getSelectedRow(); - - if (index < 0) { - return null; - } - - return ((SymbolTableModel) tabSymbols.getModel()).getSymbol(index); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/Colors.java b/src/main/java/de/vorb/tesseract/gui/view/Colors.java deleted file mode 100644 index ceb81538..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/Colors.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import java.awt.Color; - -public class Colors { - private Colors() { - } - - public static final Color NORMAL = Color.BLUE; - public static final Color SELECTION = Color.RED; - - public static final Color CORRECT = new Color(0xFF66CC00); - public static final Color INCORRECT = Color.RED; - public static final Color BASELINE = Color.BLUE; - public static final Color TEXT = Color.BLACK; - public static final Color LINE_NUMBER = Color.GRAY; -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/ComparatorPane.java b/src/main/java/de/vorb/tesseract/gui/view/ComparatorPane.java deleted file mode 100644 index 7352bfbc..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/ComparatorPane.java +++ /dev/null @@ -1,729 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import static de.vorb.tesseract.gui.view.Coordinates.scaled; -import static de.vorb.tesseract.gui.view.Coordinates.unscaled; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.FontFormatException; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.LinkedList; -import java.util.List; - -import javax.swing.DefaultComboBoxModel; -import javax.swing.ImageIcon; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSlider; -import javax.swing.JSplitPane; -import javax.swing.JTextField; -import javax.swing.SwingConstants; -import javax.swing.SwingWorker; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import de.vorb.tesseract.gui.event.ZoomChangeListener; -import de.vorb.tesseract.gui.model.PageModel; -import de.vorb.tesseract.util.Baseline; -import de.vorb.tesseract.util.Box; -import de.vorb.tesseract.util.FontAttributes; -import de.vorb.tesseract.util.Line; -import de.vorb.tesseract.util.Page; -import de.vorb.tesseract.util.Point; -import de.vorb.tesseract.util.Symbol; -import de.vorb.tesseract.util.Word; - -public class ComparatorPane extends JPanel implements ZoomChangeListener, - MainComponent { - private static final long serialVersionUID = 1L; - - private static final int SCROLL_UNITS = 12; - - // Fallback fonts - private static final Font FONT_FALLBACK_NORMAL = new Font("SansSerif", - Font.PLAIN, 12); - private static final Font FONT_FALLBACK_ITALIC = new Font("SansSerif", - Font.ITALIC, 12); - private static final Font FONT_FALLBACK_BOLD = new Font("SansSerif", - Font.BOLD, 12); - private static final Font FONT_FALLBACK_BOLD_ITALIC = new Font("SansSerif", - Font.BOLD | Font.ITALIC, 12); - - private static final Font FONT_ANTIQUA_NORMAL; - private static final Font FONT_ANTIQUA_ITALIC; - private static final Font FONT_ANTIQUA_BOLD; - private static final Font FONT_ANTIQUA_BOLD_ITALIC; - - private static final Font FONT_FRAKTUR_NORMAL; - private static final Font FONT_FRAKTUR_BOLD; - - static { - // load fonts - - // --------------------------------------------------------------------- - // ANTIQUA: - // --------------------------------------------------------------------- - - // normal - Font loaded = FONT_FALLBACK_NORMAL; - try { - loaded = Font.createFont( - Font.TRUETYPE_FONT, - ComparatorPane.class.getResourceAsStream("/RobotoCondensed-Regular.ttf")); - } catch (FontFormatException | IOException e) { - System.err.println("Could not load normal font."); - e.printStackTrace(); - } - FONT_ANTIQUA_NORMAL = loaded; - - // bold - loaded = FONT_FALLBACK_ITALIC; - try { - loaded = Font.createFont( - Font.TRUETYPE_FONT, - ComparatorPane.class.getResourceAsStream("/RobotoCondensed-Italic.ttf")); - } catch (FontFormatException | IOException e) { - System.err.println("Could not load italic font."); - } - FONT_ANTIQUA_ITALIC = loaded; - - // bold - loaded = FONT_FALLBACK_BOLD; - try { - loaded = Font.createFont( - Font.TRUETYPE_FONT, - ComparatorPane.class.getResourceAsStream("/RobotoCondensed-Bold.ttf")); - } catch (FontFormatException | IOException e) { - System.err.println("Could not load bold font."); - } - FONT_ANTIQUA_BOLD = loaded; - - // bold & italic - loaded = FONT_FALLBACK_BOLD_ITALIC; - try { - loaded = Font.createFont( - Font.TRUETYPE_FONT, - ComparatorPane.class.getResourceAsStream("/RobotoCondensed-BoldItalic.ttf")); - } catch (FontFormatException | IOException e) { - System.err.println("Could not load bold italic font."); - } - FONT_ANTIQUA_BOLD_ITALIC = loaded; - - // --------------------------------------------------------------------- - // FRAKTUR: - // --------------------------------------------------------------------- - - // normal - loaded = FONT_FALLBACK_NORMAL; - try { - loaded = Font.createFont( - Font.TRUETYPE_FONT, - ComparatorPane.class.getResourceAsStream("/UnifrakturMaguntia.ttf")); - } catch (FontFormatException | IOException e) { - System.err.println("Could not load Fraktur font."); - } - FONT_FRAKTUR_NORMAL = loaded; - - // bold - FONT_FRAKTUR_BOLD = loaded; - - // currently there is no bold Fraktur font - } - - private final JTextField tfSelection; - private final JTextField tfConfidence; - private final JCheckBox cbCorrect; - - private final JLabel lblOriginal; - private final JLabel lblHOCR; - private final JSlider zoomSlider; - - private final JCheckBox cbWordBoxes; - private final JCheckBox cbSymbolBoxes; - private final JCheckBox cbLineNumbers; - private final JCheckBox cbBaseline; - private final JCheckBox cbXLine; - private final JComboBox comboBox; - - private final LinkedList zoomChangeListeners = new LinkedList(); - - private PageModel model = new PageModel(new Page(Paths.get(""), 1, 1, 300, - new LinkedList()), new BufferedImage(1, 1, - BufferedImage.TYPE_BYTE_GRAY), new BufferedImage(1, 1, - BufferedImage.TYPE_BYTE_BINARY)); - - /** - * Create the panel. - */ - public ComparatorPane() { - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - add(panel, BorderLayout.SOUTH); - - JLabel lblSelectedWord = new JLabel("Selected word:"); - panel.add(lblSelectedWord); - - tfSelection = new JTextField(); - tfSelection.setEditable(false); - panel.add(tfSelection); - tfSelection.setColumns(20); - - JLabel lblConfidence = new JLabel("Confidence:"); - panel.add(lblConfidence); - - tfConfidence = new JTextField(); - tfConfidence.setEditable(false); - panel.add(tfConfidence); - tfConfidence.setColumns(8); - - cbCorrect = new JCheckBox("Correct?"); - cbCorrect.setEnabled(false); - cbCorrect.setToolTipText("Is the selected word correct?"); - panel.add(cbCorrect); - - JSplitPane splitPane = new JSplitPane(); - splitPane.setOneTouchExpandable(true); - splitPane.setEnabled(false); - splitPane.setResizeWeight(0.5); - add(splitPane, BorderLayout.CENTER); - - final JScrollPane spOriginal = new JScrollPane(); - spOriginal.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS); - spOriginal.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS); - splitPane.setLeftComponent(spOriginal); - - MouseListener mouseListener = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - final PageModel model = getModel(); - final Page page = model.getPage(); - - final float factor = getScaleFactor(); - - final Point scaled = new Point(e.getPoint()); - final Point unscaled = new Point( - unscaled(scaled.getX(), factor), - unscaled(scaled.getY(), factor)); - - int lineIndex = 0; - int wordIndex = 0; - - // true if clicked a box (word) - boolean hit = false; - - for (Line line : page.getLines()) { - for (Word word : line.getWords()) { - - // word.setSelected(false); - - if (word.getBoundingBox().contains(unscaled)) { - hit = true; - - if (e.getClickCount() == 2 || e.isControlDown()) { - word.setCorrect(!word.isCorrect()); - } - - tfSelection.setText(word.getText()); - final String text = word.getText(); - final StringBuilder tooltip = new StringBuilder(); - tooltip.append("[ "); - for (char c : text.toCharArray()) { - tooltip.append((int) c); - tooltip.append(' '); - } - tooltip.append(']'); - tfSelection.setToolTipText(tooltip.toString()); - tfConfidence.setText(String.valueOf(word.getConfidence())); - cbCorrect.setSelected(word.isCorrect()); - - // word.setSelected(true); - - // model.setSelectedLineIndex(lineIndex); - // model.setSelectedWordIndex(wordIndex); - } - - wordIndex++; - } - - wordIndex = 0; - lineIndex++; - } - - if (!hit) { - tfSelection.setText(""); - tfConfidence.setText(""); - cbCorrect.setSelected(false); - - // if (model.hasSelected()) { - // model.getSelected().setSelected(false); - // model.setSelectedLineIndex(-1); - // model.setSelectedWordIndex(-1); - // } - } - - render(); - } - }; - - lblOriginal = new JLabel(); - lblOriginal.addMouseListener(mouseListener); - lblOriginal.setVerticalAlignment(SwingConstants.TOP); - spOriginal.setViewportView(lblOriginal); - - final JScrollPane spHOCR = new JScrollPane(); - spHOCR.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNITS); - spHOCR.getVerticalScrollBar().setUnitIncrement(SCROLL_UNITS); - splitPane.setRightComponent(spHOCR); - - lblHOCR = new JLabel(); - lblHOCR.addMouseListener(mouseListener); - lblHOCR.setVerticalAlignment(SwingConstants.TOP); - spHOCR.setViewportView(lblHOCR); - - JPanel panel_1 = new JPanel(); - add(panel_1, BorderLayout.NORTH); - panel_1.setLayout(new BorderLayout(0, 0)); - - JPanel panel_2 = new JPanel(); - panel_1.add(panel_2, BorderLayout.EAST); - panel_2.setLayout(new FlowLayout(FlowLayout.RIGHT, 5, 5)); - - JLabel lblZoom = new JLabel("Zoom"); - panel_2.add(lblZoom); - - zoomSlider = new JSlider(); - zoomSlider.setMinimum(1); - panel_2.add(zoomSlider); - zoomSlider.setPreferredSize(new Dimension(160, 20)); - zoomSlider.setSnapToTicks(true); - zoomSlider.setMajorTickSpacing(1); - zoomSlider.setValue(5); - zoomSlider.setMaximum(9); - - JPanel panel_3 = new JPanel(); - FlowLayout flowLayout = (FlowLayout) panel_3.getLayout(); - flowLayout.setAlignment(FlowLayout.LEFT); - panel_1.add(panel_3, BorderLayout.WEST); - - final ChangeListener checkBoxListener = new ChangeListener() { - public void stateChanged(ChangeEvent ev) { - if (cbWordBoxes == ev.getSource() && cbWordBoxes.isSelected()) { - cbSymbolBoxes.setSelected(false); - } else if (cbSymbolBoxes == ev.getSource() - && cbSymbolBoxes.isSelected()) { - cbWordBoxes.setSelected(false); - } - - render(); - } - }; - - cbWordBoxes = new JCheckBox("Word boxes"); - cbWordBoxes.setSelected(true); - cbWordBoxes.addChangeListener(checkBoxListener); - panel_3.add(cbWordBoxes); - - cbSymbolBoxes = new JCheckBox("Symbol boxes"); - cbSymbolBoxes.setSelected(false); - cbSymbolBoxes.addChangeListener(checkBoxListener); - panel_3.add(cbSymbolBoxes); - - cbLineNumbers = new JCheckBox("Line numbers"); - cbLineNumbers.setSelected(true); - cbLineNumbers.addChangeListener(checkBoxListener); - panel_3.add(cbLineNumbers); - - cbBaseline = new JCheckBox("Baseline"); - cbBaseline.setSelected(false); - cbBaseline.addChangeListener(checkBoxListener); - panel_3.add(cbBaseline); - - cbXLine = new JCheckBox("x-Line"); - cbXLine.setSelected(false); - cbXLine.addChangeListener(checkBoxListener); - panel_3.add(cbXLine); - - comboBox = new JComboBox(); - comboBox.setModel(new DefaultComboBoxModel(new String[] { - "Antiqua", "Fraktur" })); - comboBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent ev) { - render(); - } - }); - - panel_3.add(comboBox); - zoomSlider.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent ev) { - zoomChanged(); - } - }); - - addZoomChangeListener(this); - - spOriginal.getViewport().addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - spHOCR.getHorizontalScrollBar().setModel( - spOriginal.getHorizontalScrollBar().getModel()); - spHOCR.getVerticalScrollBar().setModel( - spOriginal.getVerticalScrollBar().getModel()); - } - }); - - spHOCR.getViewport().addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - spOriginal.getHorizontalScrollBar().setModel( - spHOCR.getHorizontalScrollBar().getModel()); - spOriginal.getVerticalScrollBar().setModel( - spHOCR.getVerticalScrollBar().getModel()); - } - }); - } - - public PageModel getModel() { - return model; - } - - public void setModel(PageModel page) { - model = page; - - zoomChanged(zoomSlider.getValue()); - } - - public void addZoomChangeListener(ZoomChangeListener listener) { - zoomChangeListeners.add(listener); - } - - public void removeZoomChangeListener(ZoomChangeListener listener) { - zoomChangeListeners.remove(listener); - } - - private void zoomChanged() { - final int zoom = zoomSlider.getValue(); - for (ZoomChangeListener l : zoomChangeListeners) { - l.zoomChanged(zoom); - } - } - - private static class ImagePair { - final BufferedImage a; - final BufferedImage b; - - ImagePair(BufferedImage a, BufferedImage b) { - this.a = a; - this.b = b; - } - } - - private SwingWorker renderer = null; - - public void zoomChanged(final int zoom) { - render(); - } - - private void render() { - if (renderer != null && !renderer.isDone()) { - renderer.cancel(true); - } - - final int zoom = zoomSlider.getValue(); - final float factor = getScaleFactor(); - - final Page page = getModel().getPage(); - final List lines = page.getLines(); - final BufferedImage normal = getModel().getImage(); - - // font for line numbers - final Font lineNumberFont = new Font("Dialog", Font.PLAIN, 12); - - // is Fraktur selected? - final boolean useFraktur = "Fraktur".equals(comboBox.getSelectedItem()); - - // set the base fonts - final Font baseFontNormal; - final Font baseFontItalic; - final Font baseFontBold; - final Font baseFontBoldItalic; - if (useFraktur) { - baseFontNormal = FONT_FRAKTUR_NORMAL; - baseFontItalic = FONT_FRAKTUR_NORMAL; - baseFontBold = FONT_FRAKTUR_BOLD; - baseFontBoldItalic = FONT_FRAKTUR_BOLD; - } else { - baseFontNormal = FONT_ANTIQUA_NORMAL; - baseFontItalic = FONT_ANTIQUA_ITALIC; - baseFontBold = FONT_ANTIQUA_BOLD; - baseFontBoldItalic = FONT_ANTIQUA_BOLD_ITALIC; - } - - final int width = page.getWidth(); - final int height = page.getHeight(); - - // calc scaled width and height - final int scaledWidth = scaled(width, factor); - final int scaledHeight = scaled(height, factor); - - final boolean showWordBoxes = cbWordBoxes.isSelected(); - final boolean showSymbolBoxes = cbSymbolBoxes.isSelected(); - final boolean showLineNumbers = cbLineNumbers.isSelected(); - final boolean showBaselines = cbBaseline.isSelected(); - final boolean showXLines = cbXLine.isSelected(); - - renderer = new SwingWorker() { - private BufferedImage scanImg, hocrImg; - private Graphics2D scanGfx, hocrGfx; - - private void drawLineNumber(final Line line, final int lineNumber, - final Color color) { - - final Box box = line.getBoundingBox(); - - final String num = String.valueOf(lineNumber); - final int x = scaled(20, factor); - final int y = scaled(box.getY() + box.getHeight() - - line.getBaseline().getYOffset(), factor); - - scanGfx.setFont(lineNumberFont); - scanGfx.setColor(color); - scanGfx.drawString(num, x, y); - - hocrGfx.setFont(lineNumberFont); - hocrGfx.setColor(color); - hocrGfx.drawString(num, x, y); - } - - private void drawWord(final Line line, final Word word) { - // bounding box - final Box box = word.getBoundingBox(); - - // baseline - final Baseline bl = word.getBaseline(); - - // font attributes - final FontAttributes fa = word.getFontAttributes(); - - // scaled font size - final float scFontSize = scaled(fa.getSize(), factor); - - // bold? - final boolean bold = fa.isBold(); - // italic? - final boolean italic = fa.isItalic(); - - // selected? - // final boolean isSelected = word.isSelected(); - - // coordinates - final int bX = box.getX(), bY = box.getY(); - final int bW = box.getWidth(), bH = box.getHeight(); - - // scaled coordinates - final int scX = scaled(bX, factor); - final int scY = scaled(bY, factor); - final int scW = scaled(bW, factor); - final int scH = scaled(bH, factor); - - // text coordinates - final int tx = scX; - final int ty = scaled( - bY + bH - word.getBaseline().getYOffset(), - factor); - - // set font - final Font font; - if (italic && bold) { - font = baseFontBoldItalic.deriveFont(scFontSize); - } else if (italic) { - font = baseFontItalic.deriveFont(scFontSize); - } else if (bold) { - font = baseFontBold.deriveFont(scFontSize); - } else { - font = baseFontNormal.deriveFont(scFontSize); - } - - hocrGfx.setFont(font); - - if (showWordBoxes || showSymbolBoxes) { - // if (isSelected) { - // scanGfx.setStroke(STROKE_SELECTION); - // hocrGfx.setStroke(STROKE_SELECTION); - // } - - if (showWordBoxes) { - if (word.isCorrect()) { - scanGfx.setColor(Colors.CORRECT); - hocrGfx.setColor(Colors.CORRECT); - } else { - scanGfx.setColor(Colors.INCORRECT); - hocrGfx.setColor(Colors.INCORRECT); - } - - scanGfx.drawRect(scX, scY, scW, scH); - hocrGfx.drawRect(scX, scY, scW, scH); - } else if (showSymbolBoxes) { - for (final Symbol sym : word.getSymbols()) { - // symbol bounding box - final Box sbox = sym.getBoundingBox(); - - // symbol text - final String stext = sym.getText(); - - // coordinates - final int sbX = sbox.getX(); - final int sbY = sbox.getY(); - final int sbW = sbox.getWidth(); - final int sbH = sbox.getHeight(); - - // scaled coordinates - final int ssbX = scaled(sbX, factor); - final int ssbY = scaled(sbY, factor); - final int ssbW = scaled(sbW, factor); - final int ssbH = scaled(sbH, factor); - - if (word.isCorrect()) { - scanGfx.setColor(Colors.CORRECT); - hocrGfx.setColor(Colors.CORRECT); - } else { - scanGfx.setColor(Colors.INCORRECT); - hocrGfx.setColor(Colors.INCORRECT); - } - - scanGfx.drawRect(ssbX, ssbY, ssbW, ssbH); - hocrGfx.drawRect(ssbX, ssbY, ssbW, ssbH); - - hocrGfx.setColor(Colors.TEXT); - - hocrGfx.drawString( - stext, - ssbX, - scaled(box.getY() + box.getHeight() - - word.getBaseline().getYOffset(), - factor)); - } - } - - // if (isSelected) { - // scanGfx.setStroke(STROKE_NORMAL); - // hocrGfx.setStroke(STROKE_NORMAL); - // } - } - - if (!showSymbolBoxes) { - hocrGfx.setColor(Colors.TEXT); - - // only draw the string - hocrGfx.drawString( - word.getText(), tx, ty); - } - - if (showBaselines) { - final int x2 = bX + bW; - final int y1 = bY + bH - bl.getYOffset(); - final int y2 = Math.round(y1 + bW * bl.getSlope()); - - scanGfx.setColor(Colors.BASELINE); - scanGfx.drawLine(scaled(bX, factor), scaled(y1, factor), - scaled(x2, factor), scaled(y2, factor)); - hocrGfx.setColor(Colors.BASELINE); - hocrGfx.drawLine(scaled(bX, factor), scaled(y1, factor), - scaled(x2, factor), scaled(y2, factor)); - } - } - - @Override - protected ImagePair doInBackground() throws Exception { - // init attributes - scanImg = new BufferedImage(scaledWidth, scaledHeight, - BufferedImage.TYPE_INT_RGB); - scanGfx = scanImg.createGraphics(); - - hocrImg = new BufferedImage(scaledWidth, scaledHeight, - BufferedImage.TYPE_INT_RGB); - hocrGfx = hocrImg.createGraphics(); - - scanGfx.drawImage(normal, 0, 0, scaledWidth, scaledHeight, 0, - 0, width - 1, height - 1, null); - - hocrGfx.setColor(Color.WHITE); - hocrGfx.fillRect(0, 0, scaledWidth, scaledHeight); - - // stays the same for all lines - scanGfx.setFont(lineNumberFont); - - int lineNumber = 1; - for (Line line : lines) { - if (zoom >= 1 && showLineNumbers) { - drawLineNumber(line, lineNumber, Colors.LINE_NUMBER); - } - - hocrGfx.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, - RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); - - for (final Word word : line.getWords()) { - drawWord(line, word); - } - - lineNumber++; - } - - return new ImagePair(scanImg, hocrImg); - } - - @Override - protected void done() { - try { - final ImagePair pair = get(); - - lblOriginal.setIcon(new ImageIcon(pair.a)); - lblHOCR.setIcon(new ImageIcon(pair.b)); - } catch (Exception e) { - } - } - }; - - renderer.execute(); - } - - private float getScaleFactor() { - return (zoomSlider.getValue() + 1) * 0.1f; - } - - private void setAscendersEnabled(boolean enabled) { - if (!enabled) { - cbLineNumbers.setSelected(false); - cbBaseline.setSelected(false); - cbXLine.setSelected(false); - } - - cbLineNumbers.setEnabled(enabled); - cbBaseline.setEnabled(enabled); - cbXLine.setEnabled(enabled); - } - - @Override - public Component asComponent() { - return this; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/Coordinates.java b/src/main/java/de/vorb/tesseract/gui/view/Coordinates.java deleted file mode 100644 index 9557d9f7..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/Coordinates.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.vorb.tesseract.gui.view; - -public class Coordinates { - private Coordinates() { - } - - public static int scaled(int coord, float scale) { - return Math.round(coord * scale); - } - - public static int unscaled(int coord, float scale) { - return Math.round(coord / scale); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/GlyphExportPane.java b/src/main/java/de/vorb/tesseract/gui/view/GlyphExportPane.java deleted file mode 100644 index eb7a7ca8..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/GlyphExportPane.java +++ /dev/null @@ -1,142 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import javax.swing.JPanel; - -import java.awt.BorderLayout; -import java.awt.Component; - -import javax.swing.DefaultListModel; -import javax.swing.JList; -import javax.swing.JSplitPane; -import javax.swing.JButton; - -import de.vorb.tesseract.gui.model.PageModel; -import de.vorb.tesseract.gui.view.renderer.GlyphListCellRenderer; -import de.vorb.tesseract.util.Line; -import de.vorb.tesseract.util.Page; -import de.vorb.tesseract.util.Symbol; -import de.vorb.tesseract.util.Word; - -import java.awt.FlowLayout; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Set; -import java.util.TreeSet; -import java.util.Map.Entry; - -public class GlyphExportPane extends JPanel implements MainComponent { - private static final long serialVersionUID = 1L; - - private final GlyphSelectionPane glyphSelectionPane; - private final GlyphListPane glyphListPane; - - private PageModel model = null; - - public static final Comparator>> GLYPH_COMP = - new Comparator>>() { - @Override - public int compare(Entry> o1, - Entry> o2) { - return o2.getValue().size() - o1.getValue().size(); - } - }; - - public static final Comparator SYMBOL_COMP = - new Comparator() { - @Override - public int compare(Symbol o1, Symbol o2) { - if (o2.getConfidence() >= o1.getConfidence()) - return 1; - - return -1; - } - }; - - /** - * Create the panel. - */ - public GlyphExportPane() { - super(); - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - FlowLayout flowLayout = (FlowLayout) panel.getLayout(); - flowLayout.setAlignment(FlowLayout.TRAILING); - add(panel, BorderLayout.SOUTH); - - JButton btnExport = new JButton("Export ..."); - panel.add(btnExport); - - JSplitPane splitPane = new JSplitPane(); - add(splitPane, BorderLayout.CENTER); - - glyphSelectionPane = new GlyphSelectionPane(); - glyphListPane = new GlyphListPane(); - - splitPane.setLeftComponent(glyphSelectionPane); - splitPane.setRightComponent(glyphListPane); - } - - public GlyphSelectionPane getGlyphSelectionPane() { - return glyphSelectionPane; - } - - public GlyphListPane getGlyphListPane() { - return glyphListPane; - } - - @Override - public void setModel(PageModel model) { - final JList>> glyphList = - getGlyphSelectionPane().getList(); - - final HashMap> glyphs = new HashMap<>(); - - final Page page = model.getPage(); - - // set a new renderer that has a reference to the thresholded image - getGlyphListPane().getList().setCellRenderer( - new GlyphListCellRenderer(model.getBlackAndWhiteImage())); - - // insert all symbols into the map - for (final Line line : page.getLines()) { - for (final Word word : line.getWords()) { - for (final Symbol symbol : word.getSymbols()) { - final String sym = symbol.getText(); - - if (!glyphs.containsKey(sym)) { - glyphs.put(sym, new TreeSet(SYMBOL_COMP)); - } - - glyphs.get(sym).add(symbol); - } - } - } - - final LinkedList>> entries = new LinkedList<>( - glyphs.entrySet()); - - Collections.sort(entries, GLYPH_COMP); - - final DefaultListModel>> listModel = - new DefaultListModel<>(); - - for (final Entry> entry : entries) { - listModel.addElement(entry); - } - - glyphList.setModel(listModel); - } - - @Override - public PageModel getModel() { - return model; - } - - @Override - public Component asComponent() { - return this; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/GlyphListPane.java b/src/main/java/de/vorb/tesseract/gui/view/GlyphListPane.java deleted file mode 100644 index 2f33c0d7..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/GlyphListPane.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; - -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; - -import de.vorb.tesseract.util.Symbol; - -public class GlyphListPane extends JPanel { - private static final long serialVersionUID = 1L; - private final JList glyphList; - - /** - * Create the panel. - */ - public GlyphListPane() { - super(); - setLayout(new BorderLayout(0, 0)); - - JPanel panel = new JPanel(); - FlowLayout fl_panel = (FlowLayout) panel.getLayout(); - fl_panel.setAlignment(FlowLayout.LEADING); - add(panel, BorderLayout.NORTH); - - JLabel lblVariants = new JLabel("Variants"); - panel.add(lblVariants); - - JScrollPane scrollPane = new JScrollPane(); - add(scrollPane, BorderLayout.CENTER); - - glyphList = new JList(); - glyphList.setLayoutOrientation(JList.HORIZONTAL_WRAP); - scrollPane.setViewportView(glyphList); - } - - public JList getList() { - return glyphList; - } - -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java b/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java deleted file mode 100644 index 20e1c168..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/MainComponent.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import java.awt.Component; - -import de.vorb.tesseract.gui.model.PageModel; - -public interface MainComponent { - public void setModel(PageModel model); - - public PageModel getModel(); - - public Component asComponent(); -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/OpenProjectDialog.java b/src/main/java/de/vorb/tesseract/gui/view/OpenProjectDialog.java deleted file mode 100644 index fbbb4a4d..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/OpenProjectDialog.java +++ /dev/null @@ -1,201 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextField; -import javax.swing.border.EmptyBorder; - -import de.vorb.tesseract.gui.event.LocaleChangeListener; -import de.vorb.tesseract.gui.event.ProjectChangeListener; -import de.vorb.tesseract.gui.view.i18n.Labels; - -public class OpenProjectDialog extends JDialog implements LocaleChangeListener { - private static final long serialVersionUID = 1L; - - private final JPanel contentPanel = new JPanel(); - private final JTextField tfScanDir; - - private final List listeners = new LinkedList(); - - private final JButton btCancel; - private final JButton btOK; - private final JLabel lblScanDirectory; - - /** - * Launch the application. - */ - public static void main(String[] args) { - try { - OpenProjectDialog dialog = new OpenProjectDialog(null); - dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - dialog.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Create the dialog. - * - * @param owner - */ - public OpenProjectDialog(final Window owner) { - super(owner); - - setMinimumSize(new Dimension(500, 130)); - - getContentPane().setLayout(new BorderLayout()); - contentPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - getContentPane().add(contentPanel, BorderLayout.CENTER); - GridBagLayout gbl_contentPanel = new GridBagLayout(); - gbl_contentPanel.columnWidths = new int[] { 0, 0, 0, 0 }; - gbl_contentPanel.rowHeights = new int[] { 0, 0 }; - gbl_contentPanel.columnWeights = new double[] { 0.0, 1.0, 0.0, - Double.MIN_VALUE }; - gbl_contentPanel.rowWeights = new double[] { 0.0, Double.MIN_VALUE }; - contentPanel.setLayout(gbl_contentPanel); - { - lblScanDirectory = new JLabel("Scan directory:"); - GridBagConstraints gbc_lblScans = new GridBagConstraints(); - gbc_lblScans.insets = new Insets(0, 0, 0, 5); - gbc_lblScans.anchor = GridBagConstraints.EAST; - gbc_lblScans.gridx = 0; - gbc_lblScans.gridy = 0; - contentPanel.add(lblScanDirectory, gbc_lblScans); - } - { - tfScanDir = new JTextField(); - - // TODO remove - tfScanDir.setText("E:\\Masterarbeit\\Ressourcen\\DE-20__32_AM_49000_L869_G927-1\\sauvola"); - - GridBagConstraints gbc_tfScanDir = new GridBagConstraints(); - gbc_tfScanDir.insets = new Insets(0, 0, 0, 5); - gbc_tfScanDir.fill = GridBagConstraints.HORIZONTAL; - gbc_tfScanDir.gridx = 1; - gbc_tfScanDir.gridy = 0; - contentPanel.add(tfScanDir, gbc_tfScanDir); - tfScanDir.setColumns(10); - } - { - JButton button = new JButton("..."); - GridBagConstraints gbc_button = new GridBagConstraints(); - gbc_button.gridx = 2; - gbc_button.gridy = 0; - contentPanel.add(button, gbc_button); - - makePathChooser(tfScanDir, button); - } - { - JPanel buttonPane = new JPanel(); - buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); - getContentPane().add(buttonPane, BorderLayout.SOUTH); - { - btOK = new JButton(); - btOK.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - projectChanged(); - OpenProjectDialog.this.dispose(); - } - }); - btOK.setActionCommand("OK"); - buttonPane.add(btOK); - getRootPane().setDefaultButton(btOK); - } - { - btCancel = new JButton(); - btCancel.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - OpenProjectDialog.this.dispose(); - } - }); - btCancel.setActionCommand("Cancel"); - buttonPane.add(btCancel); - } - } - - localeChanged(); - - this.setResizable(false); - } - - @Override - public void localeChanged() { - setLocale(Locale.getDefault()); - - setTitle(Labels.getLabel(getLocale(), "open_dialog_title")); - - lblScanDirectory.setText(Labels.getLabel(getLocale(), "scan_dir")); - btCancel.setText(Labels.getLabel(getLocale(), "btn_cancel")); - btOK.setText(Labels.getLabel(getLocale(), "btn_ok")); - } - - private void makePathChooser(final JTextField tfPath, - final JButton btnChoosePath) { - - File dir = new File("E:\\Masterarbeit\\Ressourcen"); - if (!dir.isDirectory()) - dir = null; - final File startDir = dir; - - btnChoosePath.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - final JFileChooser dirChooser = new JFileChooser(startDir); - dirChooser.setMultiSelectionEnabled(false); - dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - - int state = dirChooser.showOpenDialog(OpenProjectDialog.this); - if (state == JFileChooser.APPROVE_OPTION) { - final File selection = dirChooser.getSelectedFile(); - tfPath.setText(selection.getAbsolutePath()); - } else if (state == JFileChooser.ERROR_OPTION) { - JOptionPane.showMessageDialog(dirChooser, - "Please select a directory", "Invalid selection", - JOptionPane.ERROR_MESSAGE); - } - } - }); - } - - public void addProjectChangeListener(ProjectChangeListener listener) { - listeners.add(listener); - } - - public void removeProjectChangeListener(ProjectChangeListener listener) { - listeners.remove(listener); - } - - private void projectChanged() { - final Path scanDir = Paths.get(tfScanDir.getText()); - - for (ProjectChangeListener l : listeners) { - l.projectChanged(scanDir); - } - } - - @Override - public void setVisible(boolean visible) { - setLocationRelativeTo(getParent()); - super.setVisible(visible); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java b/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java deleted file mode 100644 index 172959d4..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/PageListCellRenderer.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 1998, 2007, Oracle and/or its affiliates. All rights reserved. - * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. - */ - -package de.vorb.tesseract.gui.view; - -import java.awt.Color; -import java.awt.Component; -import java.nio.file.Path; - -import javax.swing.DefaultListCellRenderer; -import javax.swing.Icon; -import javax.swing.JList; -import javax.swing.border.Border; -import javax.swing.border.EmptyBorder; - -import sun.swing.DefaultLookup; - -/** - * @author Paul Vorbach - */ -public class PageListCellRenderer extends DefaultListCellRenderer -{ - private static final long serialVersionUID = 1L; - - /** - * An empty Border. This field might not be used. To change the - * Border used by this renderer override the - * getListCellRendererComponent method and set the border of the - * returned component directly. - */ - private static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1); - private static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1, - 1, 1); - protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER; - - private Border getNoFocusBorder() { - Border border = DefaultLookup.getBorder(this, ui, "List.cellNoFocusBorder"); - if (System.getSecurityManager() != null) { - if (border != null) - return border; - return SAFE_NO_FOCUS_BORDER; - } else { - if (border != null && - (noFocusBorder == null || - noFocusBorder == DEFAULT_NO_FOCUS_BORDER)) { - return border; - } - return noFocusBorder; - } - } - - public Component getListCellRendererComponent( - JList list, - Object value, - int index, - boolean isSelected, - boolean cellHasFocus) - { - setComponentOrientation(list.getComponentOrientation()); - - Color bg = null; - Color fg = null; - - JList.DropLocation dropLocation = list.getDropLocation(); - if (dropLocation != null - && !dropLocation.isInsert() - && dropLocation.getIndex() == index) { - - bg = DefaultLookup.getColor(this, ui, "List.dropCellBackground"); - fg = DefaultLookup.getColor(this, ui, "List.dropCellForeground"); - - isSelected = true; - } - - if (isSelected) { - setBackground(bg == null ? list.getSelectionBackground() : bg); - setForeground(fg == null ? list.getSelectionForeground() : fg); - } - else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - } - - if (value instanceof Icon) { - setIcon((Icon) value); - setText(""); - } else if (value instanceof Path) { - setText(((Path) value).getFileName().toString()); - } else { - setIcon(null); - setText((value == null) ? "" : value.toString()); - } - - setEnabled(list.isEnabled()); - setFont(list.getFont()); - - Border border = null; - if (cellHasFocus) { - if (isSelected) { - border = DefaultLookup.getBorder(this, ui, - "List.focusSelectedCellHighlightBorder"); - } - if (border == null) { - border = DefaultLookup.getBorder(this, ui, - "List.focusCellHighlightBorder"); - } - } else { - border = getNoFocusBorder(); - } - setBorder(border); - - return this; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java b/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java deleted file mode 100644 index 0d0547ce..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/TesseractFrame.java +++ /dev/null @@ -1,403 +0,0 @@ -package de.vorb.tesseract.gui.view; - -import java.awt.*; -import java.awt.Dialog.ModalityType; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; -import java.nio.file.Path; -import java.util.LinkedList; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; - -import com.google.common.base.Optional; - -import de.vorb.tesseract.gui.model.FilteredListModel.Filter; -import de.vorb.tesseract.gui.model.PageModel; -import de.vorb.tesseract.gui.view.FilteredList.FilterProvider; -import de.vorb.tesseract.gui.view.i18n.Labels; -import de.vorb.tesseract.util.Symbol; - -/** - * Swing component that allows to compare the results of Tesseract. - */ -public class TesseractFrame extends JFrame { - private static final long serialVersionUID = 1L; - private JLabel lbCanvasOCR; - private JLabel lbCanvasOriginal; - private final FilteredList listPages; - private final FilteredList listTrainingFiles; - private final BoxEditor trainingPane; - private final ComparatorPane recognitionPane; - private final GlyphExportPane exportPane; - private final OpenProjectDialog openProjectDialog; - - private final ButtonGroup bgrpLanguage = new ButtonGroup(); - private final JProgressBar pbLoadPage; - private final ButtonGroup bgrpView = new ButtonGroup(); - private final JSplitPane spMain; - - /** - * Create the application. - */ - public TesseractFrame() { - super(); - final Toolkit t = Toolkit.getDefaultToolkit(); - - // load and set multiple icon sizes - final List appIcons = new LinkedList(); - appIcons.add(t.getImage( - TesseractFrame.class.getResource("/logos/logo_16.png"))); - appIcons.add(t.getImage( - TesseractFrame.class.getResource("/logos/logo_96.png"))); - appIcons.add(t.getImage( - TesseractFrame.class.getResource("/logos/logo_256.png"))); - setIconImages(appIcons); - - setLocationByPlatform(true); - setMinimumSize(new Dimension(1100, 680)); - setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - - openProjectDialog = new OpenProjectDialog(this); - trainingPane = new BoxEditor(); - trainingPane.setBorder(new TitledBorder(null, "Title", - TitledBorder.LEADING, TitledBorder.TOP, null, null)); - recognitionPane = new ComparatorPane(); - exportPane = new GlyphExportPane(); - pbLoadPage = new JProgressBar(); - spMain = new JSplitPane(); - - listPages = new FilteredList(null); - listPages.setMinimumSize(new Dimension(250, 100)); - listPages.getList().setSelectionMode( - ListSelectionModel.SINGLE_SELECTION); - listPages.setBorder(BorderFactory.createTitledBorder("Page")); - - // filtered string list - listTrainingFiles = new FilteredList( - new FilterProvider() { - public Optional> getFilter(String query) { - final String[] terms = query.split("\\s+"); - - final Filter filter; - if (query.isEmpty()) { - filter = null; - } else { - // item must contain all terms in query - filter = new Filter() { - @Override - public boolean accept(String item) { - for (String term : terms) { - if (!item.contains(term)) { - return false; - } - } - return true; - } - }; - } - return Optional.fromNullable(filter); - } - }); - - listTrainingFiles.setBorder(BorderFactory.createTitledBorder("Training File")); - - exportPane.getGlyphSelectionPane().getList().addListSelectionListener( - new ListSelectionListener() { - public void valueChanged(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) { - return; - } - - @SuppressWarnings("unchecked") - final JList>> selectionList = - (JList>>) e.getSource(); - - final Set symbols = selectionList.getModel().getElementAt( - selectionList.getSelectedIndex()).getValue(); - - final DefaultListModel model = new DefaultListModel<>(); - for (final Symbol symbol : symbols) { - if (symbol.getBoundingBox().getHeight() > 0) { - model.addElement(symbol); - } - } - - exportPane.getGlyphListPane().getList().setModel( - model); - } - }); - - setTitle(Labels.getLabel(getLocale(), "frame_title")); - - // Menu - - final JMenuBar menuBar = new JMenuBar(); - setJMenuBar(menuBar); - - final JMenu mnFile = new JMenu( - Labels.getLabel(getLocale(), "menu_file")); - menuBar.add(mnFile); - - openProjectDialog.setModalityType(ModalityType.APPLICATION_MODAL); - - final JMenuItem mnOpenProject = new JMenuItem(Labels.getLabel( - getLocale(), - "menu_open_project")); - mnOpenProject.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent ev) { - openProjectDialog.setVisible(true); - } - }); - mnOpenProject.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, - InputEvent.CTRL_MASK)); - mnFile.add(mnOpenProject); - - final JMenuItem mntmOcrcomparison = new JMenuItem("OCR-Comparison"); - mnFile.add(mntmOcrcomparison); - - final JSeparator separator = new JSeparator(); - mnFile.add(separator); - - final JMenuItem mntmExit = new JMenuItem( - Labels.getLabel(getLocale(), "menu_exit")); - mntmExit.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - TesseractFrame.this.dispose(); - } - }); - mnFile.add(mntmExit); - - final JMenu mnEdit = new JMenu( - Labels.getLabel(getLocale(), "menu_edit")); - menuBar.add(mnEdit); - - final JMenu mnView = new JMenu( - Labels.getLabel(getLocale(), "menu_view")); - menuBar.add(mnView); - - final JRadioButtonMenuItem rmTraining = new JRadioButtonMenuItem( - "Training"); - bgrpView.add(rmTraining); - mnView.add(rmTraining); - - final JRadioButtonMenuItem rmRecognition = new JRadioButtonMenuItem( - "Recognition"); - bgrpView.add(rmRecognition); - mnView.add(rmRecognition); - - final JRadioButtonMenuItem rmExport = new JRadioButtonMenuItem( - "Export"); - bgrpView.add(rmExport); - mnView.add(rmExport); - - // on a view change, show the other component (export glyphs or compare - // results) - final ActionListener viewChangeListener = new ActionListener() { - public void actionPerformed(ActionEvent e) { - if (rmTraining.isSelected() - && getMainComponent() != trainingPane) { - setMainComponent(trainingPane); - } else if (rmRecognition.isSelected() - && getMainComponent() != recognitionPane) { - setMainComponent(recognitionPane); - } else if (rmExport.isSelected() - && getMainComponent() != exportPane) { - setMainComponent(exportPane); - } - } - }; - - rmTraining.addActionListener(viewChangeListener); - rmRecognition.addActionListener(viewChangeListener); - rmExport.addActionListener(viewChangeListener); - bgrpView.setSelected(rmTraining.getModel(), true); - - final JMenu mnHelp = new JMenu( - Labels.getLabel(getLocale(), "menu_help")); - menuBar.add(mnHelp); - - final JMenuItem mntmAbout = new JMenuItem(Labels.getLabel(getLocale(), - "menu_about")); - mntmAbout.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - JOptionPane.showMessageDialog(TesseractFrame.this, - Labels.getLabel(getLocale(), "about_message"), - Labels.getLabel(getLocale(), "about_title"), - JOptionPane.INFORMATION_MESSAGE); - } - }); - mnHelp.add(mntmAbout); - - // Contents - - final JPanel panel = new JPanel(); - panel.setBackground(SystemColor.menu); - panel.setBorder(new EmptyBorder(5, 5, 5, 5)); - getContentPane().add(panel, BorderLayout.SOUTH); - final GridBagLayout gbl_panel = new GridBagLayout(); - gbl_panel.columnWidths = new int[] { 111, 84, 46, 0, 46, 417, 50, 40, - 0, 0 }; - gbl_panel.rowHeights = new int[] { 14, 0 }; - gbl_panel.columnWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, - 0.0, - 0.0, 0.0, Double.MIN_VALUE }; - gbl_panel.rowWeights = new double[] { 0.0, Double.MIN_VALUE }; - panel.setLayout(gbl_panel); - - final JLabel lblProjectOverview = new JLabel(Labels.getLabel( - getLocale(), - "project_overview")); - lblProjectOverview.setFont(new Font("Tahoma", Font.BOLD, 11)); - GridBagConstraints gbc_lblProjectOverview = new GridBagConstraints(); - gbc_lblProjectOverview.anchor = GridBagConstraints.WEST; - gbc_lblProjectOverview.insets = new Insets(0, 0, 0, 5); - gbc_lblProjectOverview.gridx = 0; - gbc_lblProjectOverview.gridy = 0; - panel.add(lblProjectOverview, gbc_lblProjectOverview); - - final JLabel lblCorrectWords = new JLabel(Labels.getLabel(getLocale(), - "correct_words")); - GridBagConstraints gbc_lblCorrectWords = new GridBagConstraints(); - gbc_lblCorrectWords.fill = GridBagConstraints.VERTICAL; - gbc_lblCorrectWords.insets = new Insets(0, 0, 0, 5); - gbc_lblCorrectWords.anchor = GridBagConstraints.EAST; - gbc_lblCorrectWords.gridx = 1; - gbc_lblCorrectWords.gridy = 0; - panel.add(lblCorrectWords, gbc_lblCorrectWords); - - final JLabel label = new JLabel("0"); - label.setHorizontalAlignment(SwingConstants.LEFT); - GridBagConstraints gbc_label = new GridBagConstraints(); - gbc_label.insets = new Insets(0, 0, 0, 5); - gbc_label.anchor = GridBagConstraints.WEST; - gbc_label.gridx = 2; - gbc_label.gridy = 0; - panel.add(label, gbc_label); - - final JLabel lblIncorrectWords = new JLabel(Labels.getLabel( - getLocale(), - "incorrect_words")); - GridBagConstraints gbc_lblIncorrectWords = new GridBagConstraints(); - gbc_lblIncorrectWords.anchor = GridBagConstraints.EAST; - gbc_lblIncorrectWords.insets = new Insets(0, 0, 0, 5); - gbc_lblIncorrectWords.gridx = 3; - gbc_lblIncorrectWords.gridy = 0; - panel.add(lblIncorrectWords, gbc_lblIncorrectWords); - - final JLabel label_1 = new JLabel("0"); - GridBagConstraints gbc_label_1 = new GridBagConstraints(); - gbc_label_1.insets = new Insets(0, 0, 0, 5); - gbc_label_1.anchor = GridBagConstraints.WEST; - gbc_label_1.gridx = 4; - gbc_label_1.gridy = 0; - panel.add(label_1, gbc_label_1); - - final JLabel lblTotalWords = new JLabel(Labels.getLabel(getLocale(), - "total_words")); - GridBagConstraints gbc_lblTotalWords = new GridBagConstraints(); - gbc_lblTotalWords.anchor = GridBagConstraints.EAST; - gbc_lblTotalWords.insets = new Insets(0, 0, 0, 5); - gbc_lblTotalWords.gridx = 6; - gbc_lblTotalWords.gridy = 0; - panel.add(lblTotalWords, gbc_lblTotalWords); - - final JLabel label_2 = new JLabel("0"); - GridBagConstraints gbc_label_2 = new GridBagConstraints(); - gbc_label_2.insets = new Insets(0, 0, 0, 5); - gbc_label_2.anchor = GridBagConstraints.WEST; - gbc_label_2.gridx = 7; - gbc_label_2.gridy = 0; - panel.add(label_2, gbc_label_2); - - GridBagConstraints gbc_pbRegognitionProgress = new GridBagConstraints(); - gbc_pbRegognitionProgress.gridx = 8; - gbc_pbRegognitionProgress.gridy = 0; - panel.add(pbLoadPage, gbc_pbRegognitionProgress); - getContentPane().add(spMain, BorderLayout.CENTER); - - JTabbedPane mainTabs = new JTabbedPane(); - mainTabs.addTab( - Labels.getLabel(getLocale(), "tab_main_boxeditor"), - new ImageIcon( - getClass().getResource("/icons/table_edit.png")), - trainingPane); - - spMain.setRightComponent(mainTabs); - - JSplitPane splitPane = new JSplitPane(); - splitPane.setResizeWeight(1.0); - splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); - spMain.setLeftComponent(splitPane); - splitPane.setLeftComponent(listPages); - splitPane.setRightComponent(listTrainingFiles); - } - - private MainComponent getMainComponent() { - final Component main = spMain.getRightComponent(); - if (main instanceof MainComponent) { - return (MainComponent) main; - } else { - throw new IllegalStateException( - "The current main component is not an instance of MainComponent."); - } - } - - private void setMainComponent(MainComponent main) { - final MainComponent old = getMainComponent(); - if (main == old) { - return; - } - - main.setModel(old.getModel()); - spMain.setRightComponent(main.asComponent()); - } - - public OpenProjectDialog getLoadProjectDialog() { - return openProjectDialog; - } - - public FilteredList getPageList() { - return listPages; - } - - public FilteredList getTrainingFiles() { - return listTrainingFiles; - } - - public JLabel getCanvasOCR() { - return lbCanvasOCR; - } - - public JLabel getCanvasOriginal() { - return lbCanvasOriginal; - } - - public ComparatorPane getComparatorPane() { - return recognitionPane; - } - - public GlyphExportPane getGlyphExportPane() { - return exportPane; - } - - public JProgressBar getPageLoadProgressBar() { - return pbLoadPage; - } - - public void setModel(PageModel model) { - getMainComponent().setModel(model); - } - - public PageModel getModel() { - return getMainComponent().getModel(); - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java b/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java deleted file mode 100644 index 9a7de166..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/i18n/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * - */ -/** - * @author Paul Vorbach - * - */ -package de.vorb.tesseract.gui.view.i18n; \ No newline at end of file diff --git a/src/main/java/de/vorb/tesseract/gui/view/package-info.java b/src/main/java/de/vorb/tesseract/gui/view/package-info.java deleted file mode 100644 index 81e1dfef..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @author Paul Vorbach - * - */ -package de.vorb.tesseract.gui.view; \ No newline at end of file diff --git a/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java b/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java deleted file mode 100644 index ee14c2a5..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/renderer/BoxFileRenderer.java +++ /dev/null @@ -1,112 +0,0 @@ -package de.vorb.tesseract.gui.view.renderer; - -import static de.vorb.tesseract.gui.view.Coordinates.scaled; - -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.util.Iterator; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; - -import javax.swing.ImageIcon; -import javax.swing.JLabel; -import javax.swing.SwingWorker; - -import de.vorb.tesseract.gui.model.SingleSelectionModel; -import de.vorb.tesseract.gui.view.Colors; -import de.vorb.tesseract.gui.view.Strokes; -import de.vorb.tesseract.util.Box; -import de.vorb.tesseract.util.Iterators; -import de.vorb.tesseract.util.Page; -import de.vorb.tesseract.util.Symbol; - -public class BoxFileRenderer implements PageRenderer { - private final JLabel canvas; - private final SingleSelectionModel selectionModel; - - private SwingWorker renderWorker; - - public BoxFileRenderer(JLabel canvas, SingleSelectionModel selectionModel) { - this.canvas = canvas; - this.selectionModel = selectionModel; - } - - @Override - public void render(final Page page, final BufferedImage pageBackground, - final float scale) { - // TODO add a version of render() that takes two rectangles and a new - // box and updates the necessary area only - final int w = pageBackground.getWidth(); - final int h = pageBackground.getHeight(); - - final int scaledW = scaled(w, scale); - final int scaledH = scaled(h, scale); - - final int selectedIndex = selectionModel.getSelectedIndex(); - - // try to cancel the last rendering task - if (renderWorker != null && !renderWorker.isCancelled() - && !renderWorker.isDone()) { - renderWorker.cancel(true); - } - - renderWorker = new SwingWorker() { - @Override - protected BufferedImage doInBackground() throws Exception { - final BufferedImage rendered = new BufferedImage(scaledW, - scaledH, BufferedImage.TYPE_INT_RGB); - - final Graphics2D g2d = rendered.createGraphics(); - - // initial color & stroke - g2d.setColor(Colors.NORMAL); - g2d.setStroke(Strokes.NORMAL); - - g2d.drawImage(pageBackground, 0, 0, scaledW - 1, - scaledH - 1, - 0, 0, w - 1, h - 1, null); - - final Iterator it = Iterators.symbolIterator(page); - for (int symbolIndex = 0; it.hasNext(); symbolIndex++) { - // determine if box is selected - final boolean isSelected = selectedIndex == symbolIndex; - - // draw box on canvas - drawSymbolBox(g2d, it.next(), scale, isSelected); - } - - return rendered; - } - - @Override - public void done() { - try { - canvas.setIcon(new ImageIcon(get())); - } catch (InterruptedException | ExecutionException - | CancellationException e) { - // ignore interrupts of any kind, those are intented - } - } - }; - - renderWorker.execute(); - } - - private void drawSymbolBox(final Graphics2D g2d, final Symbol symbol, - final float scale, final boolean isSelected) { - final Box bbox = symbol.getBoundingBox(); - - if (isSelected) { - g2d.setColor(Colors.SELECTION); - g2d.setStroke(Strokes.SELECTION); - } - - g2d.drawRect(scaled(bbox.getX(), scale), scaled(bbox.getY(), scale), - scaled(bbox.getWidth(), scale), scaled(bbox.getHeight(), scale)); - - if (isSelected) { - g2d.setColor(Colors.NORMAL); - g2d.setStroke(Strokes.NORMAL); - } - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphListCellRenderer.java b/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphListCellRenderer.java deleted file mode 100644 index cd4de1df..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/renderer/GlyphListCellRenderer.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.vorb.tesseract.gui.view.renderer; - -import java.awt.Component; -import java.awt.image.BufferedImage; - -import javax.swing.BorderFactory; -import javax.swing.ImageIcon; -import javax.swing.JList; -import javax.swing.JToggleButton; -import javax.swing.ListCellRenderer; - -import de.vorb.tesseract.util.Box; -import de.vorb.tesseract.util.Symbol; - -public class GlyphListCellRenderer extends JToggleButton implements - ListCellRenderer { - private static final long serialVersionUID = 1L; - - private final BufferedImage source; - - public GlyphListCellRenderer(BufferedImage source) { - super(); - this.source = source; - setOpaque(true); - this.setSelected(true); - setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - } - - @Override - public Component getListCellRendererComponent(JList list, - Symbol value, int index, boolean isSelected, boolean cellHasFocus) { - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - } - - final Box bbox = value.getBoundingBox(); - final BufferedImage subImage = source.getSubimage(bbox.getX(), - bbox.getY(), bbox.getWidth(), bbox.getHeight()); - - System.out.println((int) value.getText().toCharArray()[0]); - - // FIXME remove! - // try { - // String c = URLEncoder.encode(value.getText(), "UTF-8"); - // - // if (c.matches("\\p{Upper}+")) { - // c = "Upper_" + c; - // } else if (c.length() == 0) { - // c = "_"; - // } - // - // final File dir = new File("C:/Users/Paul/Desktop/glyphs/" + c); - // final File file = new File(dir, index + ".png"); - // Files.createDirectories(dir.toPath()); - // ImageIO.write(subImage, "PNG", file); - // } catch (Exception e) { - // // TODO Auto-generated catch block - // e.printStackTrace(); - // } - - setIcon(new ImageIcon(subImage)); - - setToolTipText("confidence = " + value.getConfidence()); - - return this; - } -} diff --git a/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java b/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java deleted file mode 100644 index 0e830922..00000000 --- a/src/main/java/de/vorb/tesseract/gui/view/renderer/PageRenderer.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.vorb.tesseract.gui.view.renderer; - -import java.awt.image.BufferedImage; - -import de.vorb.tesseract.util.Page; - -/** - * Page renderer. - * - * @author Paul Vorbach - */ -public interface PageRenderer { - - /** - * - * Renders the information of a page on an optionally given background. - * - * @param page - * page model to render - * @param pageBackground - * background image to render below (may also be null) - * @param scale - * scaling factor - */ - public void render(Page page, BufferedImage pageBackground, float scale); -} diff --git a/src/main/java/de/vorb/tesseract/util/Project.java b/src/main/java/de/vorb/tesseract/util/Project.java deleted file mode 100644 index 386c8d6b..00000000 --- a/src/main/java/de/vorb/tesseract/util/Project.java +++ /dev/null @@ -1,73 +0,0 @@ -package de.vorb.tesseract.util; - -import java.nio.file.Path; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import de.vorb.tesseract.gui.event.PageChangeListener; - -public class Project { - private final Path scanDir; - private final List pages; - private int pageIndex = 0; - private final List pageChangeListeners; - - public Project(Path scanDir, List pages) { - this.scanDir = scanDir; - this.pages = pages; - - this.pageChangeListeners = new LinkedList(); - } - - public Path getScanDir() { - return scanDir; - } - - public List getPages() { - return Collections.unmodifiableList(this.pages); - } - - public void addPageChangeListener(PageChangeListener listener) { - pageChangeListeners.add(listener); - } - - public void removePageChangeListener(PageChangeListener listener) { - pageChangeListeners.remove(listener); - } - - public int getSelectedPageIndex() { - return pageIndex; - } - - public int getMinimumPageIndex() { - return 0; - } - - public int getMaximumPageIndex() { - return Math.max(pages.size() - 1, 0); - } - - public Path getSelectedPage() { - return pages.get(getSelectedPageIndex()); - } - - private void pageChanged() { - final int index = getSelectedPageIndex(); - - for (PageChangeListener listener : pageChangeListeners) { - listener.pageSelectionChanged(index); - } - } - - public void setSelectedPageIndex(int index) { - if (index < 0) - throw new IllegalArgumentException("index < 0"); - if (index > getMaximumPageIndex()) - throw new IllegalArgumentException("index > maxIndex"); - - pageIndex = index; - - pageChanged(); - } -} diff --git a/src/main/java/de/vorb/tesseract/util/package-info.java b/src/main/java/de/vorb/tesseract/util/package-info.java deleted file mode 100644 index 13ef97f4..00000000 --- a/src/main/java/de/vorb/tesseract/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * - */ -/** - * @author Paul Vorbach - * - */ -package de.vorb.tesseract.util; \ No newline at end of file diff --git a/src/main/resources/fonts/RobotoCondensed-Bold.ttf b/src/main/resources/fonts/RobotoCondensed-Bold.ttf deleted file mode 100644 index f0fd409e..00000000 Binary files a/src/main/resources/fonts/RobotoCondensed-Bold.ttf and /dev/null differ diff --git a/src/main/resources/fonts/RobotoCondensed-BoldItalic.ttf b/src/main/resources/fonts/RobotoCondensed-BoldItalic.ttf deleted file mode 100644 index e67b02b0..00000000 Binary files a/src/main/resources/fonts/RobotoCondensed-BoldItalic.ttf and /dev/null differ diff --git a/src/main/resources/fonts/RobotoCondensed-Italic.ttf b/src/main/resources/fonts/RobotoCondensed-Italic.ttf deleted file mode 100644 index a08414bd..00000000 Binary files a/src/main/resources/fonts/RobotoCondensed-Italic.ttf and /dev/null differ diff --git a/src/main/resources/fonts/RobotoCondensed-Regular.ttf b/src/main/resources/fonts/RobotoCondensed-Regular.ttf deleted file mode 100644 index 713fd30c..00000000 Binary files a/src/main/resources/fonts/RobotoCondensed-Regular.ttf and /dev/null differ diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 00000000..33061f7e --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,11 @@ +.cache +.classpath +.gradle +.project +.settings +*.dll +bin +build +hs_err_pid*.log +lib/ +tessdata/ diff --git a/tools/pom.xml b/tools/pom.xml new file mode 100644 index 00000000..7f6c3259 --- /dev/null +++ b/tools/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + de.vorb.tesseract + tesseract4java + 0.3.0-SNAPSHOT + + + tools + + + 4.0.0-1.4.4 + + + + + org.bytedeco.javacpp-presets + tesseract-platform + ${tesseract.version} + + + net.imagej + ij + 1.52h + + + de.biomedical-imaging.ij + ij_blob + 1.4.9 + + + de.vorb.tesseract + ocrevalUAtion + + + junit + junit + 4.13.1 + test + + + + + + central + https://repo1.maven.org/maven2 + + + imagej + https://maven.imagej.net/content/groups/public + + + + diff --git a/tools/res/overview.html b/tools/res/overview.html new file mode 100644 index 00000000..b36832ad --- /dev/null +++ b/tools/res/overview.html @@ -0,0 +1,3 @@ + +

Tools for using Tesseract from Java.

+ diff --git a/tools/src/main/java/de/vorb/tesseract/img/BinaryImage.java b/tools/src/main/java/de/vorb/tesseract/img/BinaryImage.java new file mode 100644 index 00000000..4361dd3e --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/img/BinaryImage.java @@ -0,0 +1,40 @@ +package de.vorb.tesseract.img; + +import java.awt.image.BufferedImage; + +public final class BinaryImage { + + private BinaryImage() {} + + /** + * Require img to be a binary (monochrome) image. + * + * @param img + */ + public static void requireBinary(BufferedImage img) { + if (img.getType() != BufferedImage.TYPE_BYTE_BINARY) + throw new IllegalArgumentException("binary image required"); + } + + /** + * Counts all black pixels in img. + * + * @param img + * @return number of black pixels in img. + */ + public static int weight(BufferedImage img) { + // requireBinary(img); + + final int w = img.getWidth(), h = img.getHeight(); + + int blackPixels = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + if (img.getRGB(x, y) == 0xFF_00_00_00) // check for black + blackPixels++; + } + } + + return blackPixels; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/language/TokenStreamHandler.java b/tools/src/main/java/de/vorb/tesseract/tools/language/TokenStreamHandler.java new file mode 100644 index 00000000..0632dcbb --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/language/TokenStreamHandler.java @@ -0,0 +1,8 @@ +package de.vorb.tesseract.tools.language; + + +public interface TokenStreamHandler { + void handleToken(String word); + + void handleEndOfWordStream(); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/package-info.java b/tools/src/main/java/de/vorb/tesseract/tools/package-info.java new file mode 100644 index 00000000..65644a9c --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/package-info.java @@ -0,0 +1,7 @@ +/** + * Tesseract. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.tools; + diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Batch.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Batch.java new file mode 100644 index 00000000..04215944 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Batch.java @@ -0,0 +1,62 @@ +package de.vorb.tesseract.tools.preprocessing; + +import java.io.File; +import java.io.FileFilter; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Batch utility. + * + * @author Paul Vorbach + */ +public abstract class Batch { + /** + * Applies a task to every file in a given directory that is not filtered. + * + * @param dir input directory + * @param task + * @param timeout + * @param timeUnit + * @throws InterruptedException + */ + public static void process(File dir, FileFilter filter, Batch task, + long timeout, TimeUnit timeUnit) throws InterruptedException { + if (!dir.isDirectory()) + throw new IllegalArgumentException("not a directory"); + + process(dir.listFiles(filter), task, timeout, timeUnit); + } + + /** + * Applies a task for each given file. + * + * @param files + * @param task + * @param timeout + * @param timeUnit + * @throws InterruptedException + */ + public static void process(File[] files, Batch task, long timeout, + TimeUnit timeUnit) throws InterruptedException { + final ExecutorService pool = Executors.newFixedThreadPool(Runtime + .getRuntime().availableProcessors()); + + // submit one task for each file that is not filtered + for (File f : files) { + pool.execute(task.getTask(f)); + } + + pool.shutdown(); + pool.awaitTermination(timeout, timeUnit); + } + + /** + * Returns a Runnable task for every given File. + * + * @param src source file + * @return task. + */ + public abstract Runnable getTask(final File src); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CannyEdgeDetector.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CannyEdgeDetector.java new file mode 100644 index 00000000..d107f753 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CannyEdgeDetector.java @@ -0,0 +1,605 @@ +package de.vorb.tesseract.tools.preprocessing; + +import java.awt.image.BufferedImage; +import java.util.Arrays; + +/** + *

+ * This software has been released into the public domain. + * Please read the notes in this source file for additional information. + * + *

+ *

+ *

+ * This class provides a configurable implementation of the Canny edge detection + * algorithm. This classic algorithm has a number of shortcomings, but remains + * an effective tool in many scenarios. This class is designed + * for single threaded use only. + *

+ *

+ *

+ * Sample usage: + *

+ *

+ *

+ * 
+ * //create the detector
+ * CannyEdgeDetector detector = new CannyEdgeDetector();
+ * //adjust its parameters as desired
+ * detector.setLowThreshold(0.5f);
+ * detector.setHighThreshold(1f);
+ * //apply it to an image
+ * detector.setSourceImage(frame);
+ * detector.process();
+ * BufferedImage edges = detector.getEdgesImage();
+ * 
+ * 
+ *

+ *

+ * For a more complete understanding of this edge detector's parameters consult + * an explanation of the algorithm. + *

+ * + * @author Tom Gibara + */ + +public class CannyEdgeDetector { + + // statics + + private final static float GAUSSIAN_CUT_OFF = 0.005f; + private final static float MAGNITUDE_SCALE = 100F; + private final static float MAGNITUDE_LIMIT = 1000F; + private final static int MAGNITUDE_MAX = (int) (MAGNITUDE_SCALE * MAGNITUDE_LIMIT); + + // fields + + private int height; + private int width; + private int pictureSize; + private int[] data; + private int[] magnitude; + private BufferedImage sourceImage; + private BufferedImage edgesImage; + + private float gaussianKernelRadius; + private float lowThreshold; + private float highThreshold; + private int gaussianKernelWidth; + private boolean contrastNormalized; + + private float[] xConv; + private float[] yConv; + private float[] xGradient; + private float[] yGradient; + + // constructors + + /** + * Constructs a new detector with default parameters. + */ + + public CannyEdgeDetector() { + lowThreshold = 2.5f; + highThreshold = 7.5f; + gaussianKernelRadius = 2f; + gaussianKernelWidth = 16; + contrastNormalized = false; + } + + // accessors + + /** + * The image that provides the luminance data used by this detector to + * generate edges. + * + * @return the source image, or null + */ + + public BufferedImage getSourceImage() { + return sourceImage; + } + + /** + * Specifies the image that will provide the luminance data in which edges + * will be detected. A source image must be set before the process method is + * called. + * + * @param image a source of luminance data + */ + + public void setSourceImage(BufferedImage image) { + sourceImage = image; + } + + /** + * Obtains an image containing the edges detected during the last call to + * the process method. The buffered image is an opaque image of type + * BufferedImage.TYPE_INT_ARGB in which edge pixels are white and all other + * pixels are black. + * + * @return an image containing the detected edges, or null if the process + * method has not yet been called. + */ + + public BufferedImage getEdgesImage() { + return edgesImage; + } + + /** + * Sets the edges image. Calling this method will not change the operation + * of the edge detector in any way. It is intended to provide a means by + * which the memory referenced by the detector object may be reduced. + * + * @param edgesImage expected (though not required) to be null + */ + + public void setEdgesImage(BufferedImage edgesImage) { + this.edgesImage = edgesImage; + } + + /** + * The low threshold for hysteresis. The default value is 2.5. + * + * @return the low hysteresis threshold + */ + + public float getLowThreshold() { + return lowThreshold; + } + + /** + * Sets the low threshold for hysteresis. Suitable values for this parameter + * must be determined experimentally for each application. It is nonsensical + * (though not prohibited) for this value to exceed the high threshold + * value. + * + * @param threshold a low hysteresis threshold + */ + + public void setLowThreshold(float threshold) { + if (threshold < 0) + throw new IllegalArgumentException(); + lowThreshold = threshold; + } + + /** + * The high threshold for hysteresis. The default value is 7.5. + * + * @return the high hysteresis threshold + */ + + public float getHighThreshold() { + return highThreshold; + } + + /** + * Sets the high threshold for hysteresis. Suitable values for this + * parameter must be determined experimentally for each application. It is + * nonsensical (though not prohibited) for this value to be less than the + * low threshold value. + * + * @param threshold a high hysteresis threshold + */ + + public void setHighThreshold(float threshold) { + if (threshold < 0) + throw new IllegalArgumentException(); + highThreshold = threshold; + } + + /** + * The number of pixels across which the Gaussian kernel is applied. The + * default value is 16. + * + * @return the radius of the convolution operation in pixels + */ + + public int getGaussianKernelWidth() { + return gaussianKernelWidth; + } + + /** + * The number of pixels across which the Gaussian kernel is applied. This + * implementation will reduce the radius if the contribution of pixel values + * is deemed negligible, so this is actually a maximum radius. + * + * @param gaussianKernelWidth a radius for the convolution operation in pixels, at least 2. + */ + + public void setGaussianKernelWidth(int gaussianKernelWidth) { + if (gaussianKernelWidth < 2) + throw new IllegalArgumentException(); + this.gaussianKernelWidth = gaussianKernelWidth; + } + + /** + * The radius of the Gaussian convolution kernel used to smooth the source + * image prior to gradient calculation. The default value is 16. + * + * @return the Gaussian kernel radius in pixels + */ + + public float getGaussianKernelRadius() { + return gaussianKernelRadius; + } + + /** + * Sets the radius of the Gaussian convolution kernel used to smooth the + * source image prior to gradient calculation. + * + * @return a Gaussian kernel radius in pixels, must exceed 0.1f. + */ + + public void setGaussianKernelRadius(float gaussianKernelRadius) { + if (gaussianKernelRadius < 0.1f) + throw new IllegalArgumentException(); + this.gaussianKernelRadius = gaussianKernelRadius; + } + + /** + * Whether the luminance data extracted from the source image is normalized + * by linearizing its histogram prior to edge extraction. The default value + * is false. + * + * @return whether the contrast is normalized + */ + + public boolean isContrastNormalized() { + return contrastNormalized; + } + + /** + * Sets whether the contrast is normalized + * + * @param contrastNormalized true if the contrast should be normalized, false otherwise + */ + + public void setContrastNormalized(boolean contrastNormalized) { + this.contrastNormalized = contrastNormalized; + } + + // methods + + public void process() { + width = sourceImage.getWidth(); + height = sourceImage.getHeight(); + pictureSize = width * height; + initArrays(); + readLuminance(); + if (contrastNormalized) + normalizeContrast(); + computeGradients(gaussianKernelRadius, gaussianKernelWidth); + int low = Math.round(lowThreshold * MAGNITUDE_SCALE); + int high = Math.round(highThreshold * MAGNITUDE_SCALE); + performHysteresis(low, high); + thresholdEdges(); + writeEdges(data); + } + + // private utility methods + + private void initArrays() { + if (data == null || pictureSize != data.length) { + data = new int[pictureSize]; + magnitude = new int[pictureSize]; + + xConv = new float[pictureSize]; + yConv = new float[pictureSize]; + xGradient = new float[pictureSize]; + yGradient = new float[pictureSize]; + } + } + + // NOTE: The elements of the method below (specifically the technique for + // non-maximal suppression and the technique for gradient computation) + // are derived from an implementation posted in the following forum (with + // the + // clear intent of others using the code): + // http://forum.java.sun.com/thread.jspa?threadID=546211&start=45&tstart=0 + // My code effectively mimics the algorithm exhibited above. + // Since I don't know the providence of the code that was posted it is a + // possibility (though I think a very remote one) that this code violates + // someone's intellectual property rights. If this concerns you feel free to + // contact me for an alternative, though less efficient, implementation. + + private void computeGradients(float kernelRadius, int kernelWidth) { + + // generate the gaussian convolution masks + float kernel[] = new float[kernelWidth]; + float diffKernel[] = new float[kernelWidth]; + int kwidth; + for (kwidth = 0; kwidth < kernelWidth; kwidth++) { + float g1 = gaussian(kwidth, kernelRadius); + if (g1 <= GAUSSIAN_CUT_OFF && kwidth >= 2) + break; + float g2 = gaussian(kwidth - 0.5f, kernelRadius); + float g3 = gaussian(kwidth + 0.5f, kernelRadius); + kernel[kwidth] = (g1 + g2 + g3) / 3f + / (2f * (float) Math.PI * kernelRadius * kernelRadius); + diffKernel[kwidth] = g3 - g2; + } + + int initX = kwidth - 1; + int maxX = width - (kwidth - 1); + int initY = width * (kwidth - 1); + int maxY = width * (height - (kwidth - 1)); + + // perform convolution in x and y directions + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + int index = x + y; + float sumX = data[index] * kernel[0]; + float sumY = sumX; + int xOffset = 1; + int yOffset = width; + for (; xOffset < kwidth; ) { + sumY += kernel[xOffset] + * (data[index - yOffset] + data[index + yOffset]); + sumX += kernel[xOffset] + * (data[index - xOffset] + data[index + xOffset]); + yOffset += width; + xOffset++; + } + + yConv[index] = sumY; + xConv[index] = sumX; + } + + } + + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + float sum = 0f; + int index = x + y; + for (int i = 1; i < kwidth; i++) + sum += diffKernel[i] + * (yConv[index - i] - yConv[index + i]); + + xGradient[index] = sum; + } + + } + + for (int x = kwidth; x < width - kwidth; x++) { + for (int y = initY; y < maxY; y += width) { + float sum = 0.0f; + int index = x + y; + int yOffset = width; + for (int i = 1; i < kwidth; i++) { + sum += diffKernel[i] + * (xConv[index - yOffset] - xConv[index + yOffset]); + yOffset += width; + } + + yGradient[index] = sum; + } + + } + + initX = kwidth; + maxX = width - kwidth; + initY = width * kwidth; + maxY = width * (height - kwidth); + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + int index = x + y; + int indexN = index - width; + int indexS = index + width; + int indexW = index - 1; + int indexE = index + 1; + int indexNW = indexN - 1; + int indexNE = indexN + 1; + int indexSW = indexS - 1; + int indexSE = indexS + 1; + + float xGrad = xGradient[index]; + float yGrad = yGradient[index]; + float gradMag = hypot(xGrad, yGrad); + + // perform non-maximal suppression + float nMag = hypot(xGradient[indexN], yGradient[indexN]); + float sMag = hypot(xGradient[indexS], yGradient[indexS]); + float wMag = hypot(xGradient[indexW], yGradient[indexW]); + float eMag = hypot(xGradient[indexE], yGradient[indexE]); + float neMag = hypot(xGradient[indexNE], yGradient[indexNE]); + float seMag = hypot(xGradient[indexSE], yGradient[indexSE]); + float swMag = hypot(xGradient[indexSW], yGradient[indexSW]); + float nwMag = hypot(xGradient[indexNW], yGradient[indexNW]); + float tmp; + /* + * An explanation of what's happening here, for those who want + * to understand the source: This performs the "non-maximal + * suppression" phase of the Canny edge detection in which we + * need to compare the gradient magnitude to that in the + * direction of the gradient; only if the value is a local + * maximum do we consider the point as an edge candidate. + * + * We need to break the comparison into a number of different + * cases depending on the gradient direction so that the + * appropriate values can be used. To avoid computing the + * gradient direction, we use two simple comparisons: first we + * check that the partial derivatives have the same sign (1) and + * then we check which is larger (2). As a consequence, we have + * reduced the problem to one of four identical cases that each + * test the central gradient magnitude against the values at two + * points with 'identical support'; what this means is that the + * geometry required to accurately interpolate the magnitude of + * gradient function at those points has an identical geometry + * (up to right-angled-rotation/reflection). + * + * When comparing the central gradient to the two interpolated + * values, we avoid performing any divisions by multiplying both + * sides of each inequality by the greater of the two partial + * derivatives. The common comparand is stored in a temporary + * variable (3) and reused in the mirror case (4). + */ + if (xGrad * yGrad <= (float) 0 /* (1) */ + ? Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ + ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad + * neMag - (xGrad + yGrad) * eMag) /* (3) */ + && tmp > Math.abs(yGrad * swMag + - (xGrad + yGrad) * wMag) /* (4) */ + : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad + * neMag - (yGrad + xGrad) * nMag) /* (3) */ + && tmp > Math.abs(xGrad * swMag + - (yGrad + xGrad) * sMag) /* (4) */ + : Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ + ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad + * seMag + (xGrad - yGrad) * eMag) /* (3) */ + && tmp > Math.abs(yGrad * nwMag + + (xGrad - yGrad) * wMag) /* (4) */ + : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad + * seMag + (yGrad - xGrad) * sMag) /* (3) */ + && tmp > Math.abs(xGrad * nwMag + + (yGrad - xGrad) * nMag) /* (4) */ + ) { + magnitude[index] = gradMag >= MAGNITUDE_LIMIT ? MAGNITUDE_MAX + : (int) (MAGNITUDE_SCALE * gradMag); + // NOTE: The orientation of the edge is not employed by this + // implementation. It is a simple matter to compute it at + // this point as: Math.atan2(yGrad, xGrad); + } else { + magnitude[index] = 0; + } + } + } + } + + // NOTE: It is quite feasible to replace the implementation of this method + // with one which only loosely approximates the hypot function. I've tested + // simple approximations such as Math.abs(x) + Math.abs(y) and they work + // fine. + private float hypot(float x, float y) { + return (float) Math.hypot(x, y); + } + + private float gaussian(float x, float sigma) { + return (float) Math.exp(-(x * x) / (2f * sigma * sigma)); + } + + private void performHysteresis(int low, int high) { + // NOTE: this implementation reuses the data array to store both + // luminance data from the image, and edge intensity from the + // processing. + // This is done for memory efficiency, other implementations may wish + // to separate these functions. + Arrays.fill(data, 0); + + int offset = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (data[offset] == 0 && magnitude[offset] >= high) { + follow(x, y, offset, low); + } + offset++; + } + } + } + + private void follow(int x1, int y1, int i1, int threshold) { + int x0 = x1 == 0 ? x1 : x1 - 1; + int x2 = x1 == width - 1 ? x1 : x1 + 1; + int y0 = y1 == 0 ? y1 : y1 - 1; + int y2 = y1 == height - 1 ? y1 : y1 + 1; + + data[i1] = magnitude[i1]; + for (int x = x0; x <= x2; x++) { + for (int y = y0; y <= y2; y++) { + int i2 = x + y * width; + if ((y != y1 || x != x1) + && data[i2] == 0 + && magnitude[i2] >= threshold) { + follow(x, y, i2, threshold); + return; + } + } + } + } + + private void thresholdEdges() { + for (int i = 0; i < pictureSize; i++) { + data[i] = data[i] > 0 ? -1 : 0xff000000; + } + } + + private int luminance(float r, float g, float b) { + return Math.round(0.299f * r + 0.587f * g + 0.114f * b); + } + + private void readLuminance() { + int type = sourceImage.getType(); + if (type == BufferedImage.TYPE_INT_RGB + || type == BufferedImage.TYPE_INT_ARGB) { + int[] pixels = (int[]) sourceImage.getData().getDataElements(0, 0, + width, height, null); + for (int i = 0; i < pictureSize; i++) { + int p = pixels[i]; + int r = (p & 0xff0000) >> 16; + int g = (p & 0xff00) >> 8; + int b = p & 0xff; + data[i] = luminance(r, g, b); + } + } else if (type == BufferedImage.TYPE_BYTE_GRAY) { + byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, + 0, width, height, null); + for (int i = 0; i < pictureSize; i++) { + data[i] = (pixels[i] & 0xff); + } + } else if (type == BufferedImage.TYPE_USHORT_GRAY) { + short[] pixels = (short[]) sourceImage.getData().getDataElements(0, + 0, width, height, null); + for (int i = 0; i < pictureSize; i++) { + data[i] = (pixels[i] & 0xffff) / 256; + } + } else if (type == BufferedImage.TYPE_3BYTE_BGR) { + byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, + 0, width, height, null); + int offset = 0; + for (int i = 0; i < pictureSize; i++) { + int b = pixels[offset++] & 0xff; + int g = pixels[offset++] & 0xff; + int r = pixels[offset++] & 0xff; + data[i] = luminance(r, g, b); + } + } else { + throw new IllegalArgumentException("Unsupported image type: " + + type); + } + } + + private void normalizeContrast() { + int[] histogram = new int[256]; + for (int item : data) { + histogram[item]++; + } + int[] remap = new int[256]; + int sum = 0; + int j = 0; + for (int i = 0; i < histogram.length; i++) { + sum += histogram[i]; + int target = sum * 255 / pictureSize; + for (int k = j + 1; k <= target; k++) { + remap[k] = i; + } + j = target; + } + + for (int i = 0; i < data.length; i++) { + data[i] = remap[data[i]]; + } + } + + private void writeEdges(int pixels[]) { + // NOTE: There is currently no mechanism for obtaining the edge data + // in any other format other than an INT_ARGB type BufferedImage. + // This may be easily remedied by providing alternative accessors. + if (edgesImage == null) { + edgesImage = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + } + edgesImage.getWritableTile(0, 0).setDataElements(0, 0, width, height, + pixels); + } + +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CustomCannyEdgeDetector.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CustomCannyEdgeDetector.java new file mode 100644 index 00000000..2d28b16f --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/CustomCannyEdgeDetector.java @@ -0,0 +1,401 @@ +package de.vorb.tesseract.tools.preprocessing; + +import java.awt.image.BufferedImage; +import java.util.Arrays; + +/** + * Canny edge detector. + *

+ * This class is based on the implementation by Tom Gibara, which has been + * released into the public domain. Please read the notes in this source + * file for additional information. + *

+ * This class provides a configurable implementation of the Canny edge detection + * algorithm. This class is designed for single threaded use only. + *

+ *

+ * Sample usage: + *

+ *

+ *

+ * 
+ * //create the detector
+ * CannyEdgeDetector detector = new CannyEdgeDetector();
+ * //adjust its parameters as desired
+ * detector.setLowThreshold(0.5f);
+ * detector.setHighThreshold(1f);
+ * //apply it to an image
+ * detector.setSourceImage(frame);
+ * detector.process();
+ * BufferedImage edges = detector.getEdgesImage();
+ * 
+ * 
+ * + * @author Tom Gibara + * @author Paul Vorbach + */ +public class CustomCannyEdgeDetector { + // constants + public final static float GAUSSIAN_CUTOFF = 0.005f; + public final static float MAGNITUDE_SCALE = 100f; + public final static float MAGNITUDE_LIMIT = 1000f; + public final static int MAGNITUDE_MAX = (int) (MAGNITUDE_SCALE * MAGNITUDE_LIMIT); + + // configuration fields + private final float lowThreshold; + private final float highThreshold; + private final float kernelRadius; + private final int kernelWidth; + private final boolean contrastNormalized; + + /** + * Constructor using the default values. + */ + public CustomCannyEdgeDetector() { + lowThreshold = 2.5f; + highThreshold = 7.5f; + kernelRadius = 2f; + kernelWidth = 16; + contrastNormalized = false; + } + + /** + * Use custom settings. + * + * @param lowThreshold must not be negative + * @param highThreshold must not be negative + * @param gaussianKernelRadius must be >= 0.1 + * @param gaussianKernelWidth must be >= 2 + * @param contrastNormalized + */ + public CustomCannyEdgeDetector( + float lowThreshold, + float highThreshold, + float gaussianKernelRadius, + int gaussianKernelWidth, + boolean contrastNormalized) { + if (lowThreshold < 0) + throw new IllegalArgumentException("low threshold < 0"); + else + this.lowThreshold = lowThreshold; + + if (highThreshold < 0) + throw new IllegalArgumentException("high threshold < 0"); + else + this.highThreshold = highThreshold; + + if (gaussianKernelRadius < 0.1f) + throw new IllegalArgumentException("kernel radius < 0.1"); + else + this.kernelRadius = gaussianKernelRadius; + + if (gaussianKernelWidth < 2) + throw new IllegalArgumentException("kernel width < 2"); + else + this.kernelWidth = gaussianKernelWidth; + + this.contrastNormalized = contrastNormalized; + } + + /** + * Detects the edges using Canny edge detection. + * + * @param src input image - either an (A)RGB or grayscale image + * @return binary image with black edges on white + */ + public BufferedImage detectEdges(BufferedImage src) { + /* + * Preparations. + */ + final int width = src.getWidth(); + final int height = src.getHeight(); + + // number of pixels in the image + final int size = width * height; + + // prepare resulting image + final BufferedImage out = new BufferedImage(width, height, + BufferedImage.TYPE_BYTE_BINARY); + + // initialize the temporary arrays + final int[] magnitude = new int[size]; + + final float[] xConv = new float[size]; + final float[] yConv = new float[size]; + + final float[] xGradient = new float[size]; + final float[] yGradient = new float[size]; + + /* + * Get the image data as a byte array. + */ + byte[] data; + final int imageType = src.getType(); + switch (imageType) { + + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_INT_ARGB: + // retrieve the image data as an int[] + final int[] rgb = (int[]) src.getData().getDataElements(0, 0, + width, height, null); + + data = new byte[size]; + // get the luminance for every pixel + for (int i = 0; i < size; i++) { + data[i] = rgbToLuminance(rgb[i]); + } + break; + + case BufferedImage.TYPE_BYTE_GRAY: + data = (byte[]) src.getData().getDataElements(0, 0, width, + height, null); + break; + + default: + throw new IllegalArgumentException("unsupported image type"); + } + + // TODO + // if (contrastNormalized) { + // normalizeContrast(); + // } + + /* + * compute gradients + */ + + // generate the gaussian convolution masks + final float[] kernel = new float[kernelWidth]; + final float[] diffKernel = new float[kernelWidth]; + int k = 0; + for (; k < kernelWidth; k++) { + final float g1 = gaussian(k, kernelRadius); + + if (g1 <= GAUSSIAN_CUTOFF && k >= 2) + break; + + final float g2 = gaussian(k - 0.5f, kernelRadius); + final float g3 = gaussian(k + 0.5f, kernelRadius); + + kernel[k] = (g1 + g2 + g3) / 3f + / (2f * (float) Math.PI * kernelRadius * kernelRadius); + diffKernel[k] = g3 - g2; + } + + int initX = k - 1; + int maxX = width - (k - 1); + int initY = width * (k - 1); + int maxY = width * (height - (k - 1)); + + // perform convolution in x and y directions + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + int index = x + y; + float sumX = data[index] * kernel[0]; + float sumY = sumX; + int xOffset = 1; + int yOffset = width; + + for (; xOffset < k; ) { + sumY += kernel[xOffset] + * (data[index - yOffset] + data[index + yOffset]); + sumX += kernel[xOffset] + * (data[index - xOffset] + data[index + xOffset]); + yOffset += width; + xOffset++; + } + + yConv[index] = sumY; + xConv[index] = sumX; + } + } + + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + float sum = 0f; + int index = x + y; + for (int i = 1; i < k; i++) + sum += diffKernel[i] + * (yConv[index - i] - yConv[index + i]); + + xGradient[index] = sum; + } + + } + + for (int x = k; x < width - k; x++) { + for (int y = initY; y < maxY; y += width) { + float sum = 0.0f; + int index = x + y; + int yOffset = width; + for (int i = 1; i < k; i++) { + sum += diffKernel[i] + * (xConv[index - yOffset] - xConv[index + yOffset]); + yOffset += width; + } + + yGradient[index] = sum; + } + + } + + initX = k; + maxX = width - k; + initY = width * k; + maxY = width * (height - k); + for (int x = initX; x < maxX; x++) { + for (int y = initY; y < maxY; y += width) { + int index = x + y; + int indexN = index - width; + int indexS = index + width; + int indexW = index - 1; + int indexE = index + 1; + int indexNW = indexN - 1; + int indexNE = indexN + 1; + int indexSW = indexS - 1; + int indexSE = indexS + 1; + + float xGrad = xGradient[index]; + float yGrad = yGradient[index]; + float gradMag = hypot(xGrad, yGrad); + + // perform non-maximal supression + float nMag = hypot(xGradient[indexN], yGradient[indexN]); + float sMag = hypot(xGradient[indexS], yGradient[indexS]); + float wMag = hypot(xGradient[indexW], yGradient[indexW]); + float eMag = hypot(xGradient[indexE], yGradient[indexE]); + float neMag = hypot(xGradient[indexNE], yGradient[indexNE]); + float seMag = hypot(xGradient[indexSE], yGradient[indexSE]); + float swMag = hypot(xGradient[indexSW], yGradient[indexSW]); + float nwMag = hypot(xGradient[indexNW], yGradient[indexNW]); + float tmp; + /* + * An explanation of what's happening here, for those who want + * to understand the source: This performs the "non-maximal + * supression" phase of the Canny edge detection in which we + * need to compare the gradient magnitude to that in the + * direction of the gradient; only if the value is a local + * maximum do we consider the point as an edge candidate. + * + * We need to break the comparison into a number of different + * cases depending on the gradient direction so that the + * appropriate values can be used. To avoid computing the + * gradient direction, we use two simple comparisons: first we + * check that the partial derivatives have the same sign (1) and + * then we check which is larger (2). As a consequence, we have + * reduced the problem to one of four identical cases that each + * test the central gradient magnitude against the values at two + * points with 'identical support'; what this means is that the + * geometry required to accurately interpolate the magnitude of + * gradient function at those points has an identical geometry + * (upto right-angled-rotation/reflection). + * + * When comparing the central gradient to the two interpolated + * values, we avoid performing any divisions by multiplying both + * sides of each inequality by the greater of the two partial + * derivatives. The common comparand is stored in a temporary + * variable (3) and reused in the mirror case (4). + */ + if (xGrad * yGrad <= (float) 0 /* (1) */ + ? Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ + ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad + * neMag - (xGrad + yGrad) * eMag) /* (3) */ + && tmp > Math.abs(yGrad * swMag + - (xGrad + yGrad) * wMag) /* (4) */ + : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad + * neMag - (yGrad + xGrad) * nMag) /* (3) */ + && tmp > Math.abs(xGrad * swMag + - (yGrad + xGrad) * sMag) /* (4) */ + : Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ + ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad + * seMag + (xGrad - yGrad) * eMag) /* (3) */ + && tmp > Math.abs(yGrad * nwMag + + (xGrad - yGrad) * wMag) /* (4) */ + : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad + * seMag + (yGrad - xGrad) * sMag) /* (3) */ + && tmp > Math.abs(xGrad * nwMag + + (yGrad - xGrad) * nMag) /* (4) */ + ) { + magnitude[index] = gradMag >= MAGNITUDE_LIMIT ? MAGNITUDE_MAX + : (int) (MAGNITUDE_SCALE * gradMag); + // NOTE: The orientation of the edge is not employed by this + // implementation. It is a simple matter to compute it at + // this point as: Math.atan2(yGrad, xGrad); + } else { + magnitude[index] = 0; + } + } + } + + final int low = Math.round(lowThreshold * MAGNITUDE_SCALE); + final int high = Math.round(lowThreshold * MAGNITUDE_SCALE); + + Arrays.fill(data, (byte) 0); + + int offset = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (data[offset] == 0 && magnitude[offset] >= high) { + follow(x, y, offset, low); + } + offset++; + } + } + + return out; + } + + private void follow(int x1, int y1, int i1, int threshold) { + // int x0 = x1 == 0 ? x1 : x1 - 1; + // int x2 = x1 == width - 1 ? x1 : x1 + 1; + // int y0 = y1 == 0 ? y1 : y1 - 1; + // int y2 = y1 == height - 1 ? y1 : y1 + 1; + // + // data[i1] = magnitude[i1]; + // for (int x = x0; x <= x2; x++) { + // for (int y = y0; y <= y2; y++) { + // int i2 = x + y * width; + // if ((y != y1 || x != x1) + // && data[i2] == 0 + // && magnitude[i2] >= threshold) { + // follow(x, y, i2, threshold); + // return; + // } + // } + // } + } + + private byte rgbToLuminance(int rgb) { + final short r = (short) ((rgb & 0xFF0000) >> 16); + final short g = (short) ((rgb & 0x00FF00) >> 8); + final short b = (short) ((rgb & 0x0000FF) >> 0); + return (byte) (Math.round(0.299f * r + 0.587f * g + 0.114f * b) & 0xFF); + } + + private float gaussian(float x, float sigma) { + return (float) Math.exp(-(x * x) / (2f * sigma * sigma)); + } + + private float hypot(float x, float y) { + return (float) Math.hypot(x, y); + } + + /** + * Byte comparison that takes bytes as if they were unsigned. + * + * @param a + * @param b + * @return + */ + private boolean unsignedByteLT(byte a, byte b) { + if (a >= 0 && b >= 0) { + return a < b; + } else if (a < 0 && b >= 0) { + return true; + } else if (a >= 0 && b < 0) { + return false; + } else { + return b < a; + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/DefaultPreprocessor.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/DefaultPreprocessor.java new file mode 100644 index 00000000..534af7f8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/DefaultPreprocessor.java @@ -0,0 +1,49 @@ +package de.vorb.tesseract.tools.preprocessing; + +import de.vorb.tesseract.tools.preprocessing.binarization.Binarization; +import de.vorb.tesseract.tools.preprocessing.binarization.Otsu; +import de.vorb.tesseract.tools.preprocessing.filter.ImageFilter; + +import java.awt.image.BufferedImage; +import java.util.Collections; +import java.util.List; + +public class DefaultPreprocessor implements Preprocessor { + private final Binarization binarization; + private final List filters; + + public DefaultPreprocessor(Binarization binarization, + List filters) { + this.binarization = binarization; + this.filters = filters; + } + + public DefaultPreprocessor() { + this(new Otsu(), Collections.emptyList()); + } + + public DefaultPreprocessor(Binarization binarization) { + this(binarization, Collections.emptyList()); + } + + @Override + public BufferedImage process(BufferedImage image) { + // apply binarization + final BufferedImage result = binarization.binarize(image); + + // apply filters + for (final ImageFilter f : filters) { + f.filter(result); + } + + return result; + } + + public Binarization getBinarization() { + return binarization; + } + + public List getFilters() { + return Collections.unmodifiableList(filters); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Preprocessor.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Preprocessor.java new file mode 100644 index 00000000..df3ea136 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/Preprocessor.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.tools.preprocessing; + +import java.awt.image.BufferedImage; + +public interface Preprocessor { + BufferedImage process(BufferedImage image); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Binarization.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Binarization.java new file mode 100644 index 00000000..7638bcca --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Binarization.java @@ -0,0 +1,18 @@ +package de.vorb.tesseract.tools.preprocessing.binarization; + +import java.awt.image.BufferedImage; + +/** + * Binarization algorithm. + * + * @author Paul Vorbach + */ +public interface Binarization { + /** + * Binarize an image. + * + * @param image input image + * @return binary image + */ + BufferedImage binarize(BufferedImage image); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationMethod.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationMethod.java new file mode 100644 index 00000000..e56329ab --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationMethod.java @@ -0,0 +1,17 @@ +package de.vorb.tesseract.tools.preprocessing.binarization; + +public enum BinarizationMethod { + OTSU("Otsu"), + SAUVOLA("Sauvola"); + + private String name; + + BinarizationMethod(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationUtilities.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationUtilities.java new file mode 100644 index 00000000..4882d8c7 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/BinarizationUtilities.java @@ -0,0 +1,39 @@ +package de.vorb.tesseract.tools.preprocessing.binarization; + +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; + +public final class BinarizationUtilities { + + private BinarizationUtilities() { + } + + private static final ColorConvertOp RGB_TO_GRAYSCALE = new ColorConvertOp( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + + static BufferedImage imageToGrayscale(BufferedImage image) { + final BufferedImage grayscale; + + switch (image.getType()) { + case BufferedImage.TYPE_BYTE_BINARY: + return image; + case BufferedImage.TYPE_BYTE_GRAY: + grayscale = image; + break; + case BufferedImage.TYPE_INT_RGB: + case BufferedImage.TYPE_BYTE_INDEXED: + case BufferedImage.TYPE_3BYTE_BGR: + case BufferedImage.TYPE_4BYTE_ABGR: + grayscale = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + RGB_TO_GRAYSCALE.filter(image, grayscale); + break; + default: + throw new IllegalArgumentException( + "illegal color space: " + image.getColorModel().getColorSpace().getType()); + } + + return grayscale; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Otsu.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Otsu.java new file mode 100644 index 00000000..ca41c1e8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Otsu.java @@ -0,0 +1,97 @@ +package de.vorb.tesseract.tools.preprocessing.binarization; + +import java.awt.image.BufferedImage; + +public class Otsu implements Binarization { + public Otsu() { // Otsu doesn't take parameters + } + + @Override + public BufferedImage binarize(BufferedImage image) { + final int width = image.getWidth(); + final int height = image.getHeight(); + + final BufferedImage grayscale = + BinarizationUtilities.imageToGrayscale(image); + + final int[] histogram = getHistogram(grayscale); + final int threshold = getOtsuThreshold(histogram, width, height); + + final BufferedImage result = new BufferedImage(width, height, + BufferedImage.TYPE_BYTE_BINARY); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int value = grayscale.getRGB(x, y) & 0xFF; + + if (value > threshold) { + result.setRGB(x, y, 0xFFFFFFFF); + } else { + result.setRGB(x, y, 0xFF000000); + } + } + } + + return result; + } + + private static int[] getHistogram(BufferedImage grayscale) { + final int width = grayscale.getWidth(); + final int height = grayscale.getHeight(); + + final int[] histogram = new int[256]; + + for (int i = 0; i < histogram.length; i++) + histogram[i] = 0; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int value = grayscale.getRGB(x, y) & 0xFF; + histogram[value]++; + } + } + + return histogram; + } + + private static int getOtsuThreshold(int[] histogram, int width, int height) { + final int total = width * height; + + float sum = 0; + for (int i = 0; i < 256; i++) { + sum += i * histogram[i]; + } + + float sumB = 0; + int wB = 0; + int wF; + + float varMax = 0; + int threshold = 0; + + for (int i = 0; i < 256; i++) { + wB += histogram[i]; + if (wB == 0) { + continue; + } + + wF = total - wB; + + if (wF == 0) + break; + + sumB += (float) (i * histogram[i]); + float mB = sumB / wB; + float mF = (sum - sumB) / wF; + + float varBetween = (float) wB * (float) wF * (mB - mF) * (mB - mF); + + if (varBetween > varMax) { + varMax = varBetween; + threshold = i; + } + } + + return threshold; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Sauvola.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Sauvola.java new file mode 100644 index 00000000..6402c632 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/binarization/Sauvola.java @@ -0,0 +1,97 @@ +package de.vorb.tesseract.tools.preprocessing.binarization; + +import ij.plugin.filter.RankFilters; +import ij.process.FloatProcessor; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; + +/** + * Sauvola's method. + * + * @author Paul Vorbach + */ +public class Sauvola implements Binarization { + private static final int FOREGROUND = 0xFFFFFFFF; + private static final int BACKGROUND = 0xFF000000; + + private final int radius; + private final float k; + private final float R; + + /** + * Creates a new binarization configuration for binarizing image's with + * Sauvola's method. + * + * @param radius + * @param k + * @param R + * @see Sauvola + * et al. 2000 - Adaptive document image binarization + */ + public Sauvola(int radius, float k, float R) { + this.radius = radius; + this.k = k; + this.R = R; + } + + public Sauvola(int radius) { + this(radius, 0.5F, 128F); + } + + public Sauvola() { + this(15); + } + + public int getRadius() { + return radius; + } + + @Override + public BufferedImage binarize(BufferedImage image) { + final int width = image.getWidth(); + final int height = image.getHeight(); + + final BufferedImage grayscale = + BinarizationUtilities.imageToGrayscale(image); + + final BufferedImage result = new BufferedImage(width, height, + BufferedImage.TYPE_BYTE_BINARY); + + final FloatProcessor mean = new FloatProcessor(width, height); + final FloatProcessor var = (FloatProcessor) mean.duplicate(); + + final byte[] pxs = ((DataBufferByte) grayscale.getRaster() + .getDataBuffer()).getData(); + final float[] meanPxs = (float[]) mean.getPixels(); + final float[] varPxs = (float[]) var.getPixels(); + + // fill + for (int i = 0; i < pxs.length; i++) { + meanPxs[i] = pxs[i] & 0xFF; + varPxs[i] = pxs[i] & 0xFF; + } + + // pre-calculate mean and variance/std deviation + final RankFilters rankFilters = new RankFilters(); + rankFilters.rank(mean, radius, RankFilters.MEAN); + rankFilters.rank(var, radius, RankFilters.VARIANCE); + + // binarization + for (int y = 0, offset = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int i = offset + x; + if ((pxs[i] & 0xFF) > meanPxs[i] + * (1.0 + k * ((Math.sqrt(varPxs[i]) / R) - 1.0))) { + result.setRGB(x, y, FOREGROUND); + } else { + result.setRGB(x, y, BACKGROUND); + } + } + offset += width; + } + + return result; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponent.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponent.java new file mode 100644 index 00000000..cde82291 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponent.java @@ -0,0 +1,98 @@ +package de.vorb.tesseract.tools.preprocessing.conncomp; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class ConnectedComponent { + private final int label; + private final Polygon outerContour; + private final List innerContours; + + private int area = -1; + + public ConnectedComponent(int label, Polygon outerContour) { + this.label = label; + this.outerContour = outerContour; + this.innerContours = new LinkedList<>(); + } + + public void addInnerContour(Polygon innerContour) { + this.innerContours.add(innerContour); + } + + public int getLabel() { + return label; + } + + public Polygon getOuterContour() { + return outerContour; + } + + public List getInnerContours() { + return Collections.unmodifiableList(innerContours); + } + + public int getArea() { + if (area == -1) { + area = areaOfContour(outerContour); + for (final Polygon contour : innerContours) { + area -= areaOfContour(contour); + area += contour.npoints; + } + } + + return area; + } + + /** + * Count the number of pixels that are part of a contour + * + * @param contour + * @return area of contour + */ + private static int areaOfContour(Polygon contour) { + final int minY = (int) contour.getBounds().getMinY(); + final int height = (int) contour.getBounds().getHeight() + 1; + + final int[] left = new int[height]; + final int[] right = new int[height]; + + Arrays.fill(left, Integer.MAX_VALUE); + + for (int i = 0; i < contour.npoints; i++) { + final int y = contour.ypoints[i] - minY; + final int x = contour.xpoints[i]; + left[y] = Math.min(left[y], x); + right[y] = Math.max(right[y], x); + } + + int sum = 0; + for (int y = 0; y < height; y++) { + sum += right[y] - left[y] + 1; + } + + return sum; + } + + public void drawOn(Graphics2D g2d) { + g2d.setColor(Color.BLACK); + g2d.drawPolygon(outerContour); + g2d.fillPolygon(outerContour); + for (int i = 0; i < outerContour.npoints; i++) { + g2d.drawLine(outerContour.xpoints[i], outerContour.ypoints[i], + outerContour.xpoints[i], outerContour.ypoints[i]); + } + + for (final Polygon inner : innerContours) { + g2d.setColor(Color.WHITE); + g2d.fillPolygon(inner); + g2d.setColor(Color.BLACK); + g2d.drawPolygon(inner); + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentFilter.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentFilter.java new file mode 100644 index 00000000..548c10dc --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentFilter.java @@ -0,0 +1,5 @@ +package de.vorb.tesseract.tools.preprocessing.conncomp; + +public interface ConnectedComponentFilter { + boolean filter(ConnectedComponent connComp); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentLabeler.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentLabeler.java new file mode 100644 index 00000000..ce34b271 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/ConnectedComponentLabeler.java @@ -0,0 +1,246 @@ +package de.vorb.tesseract.tools.preprocessing.conncomp; + +import ij.process.ColorProcessor; + +import java.awt.Polygon; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class ConnectedComponentLabeler { + private static final ConnectedComponentFilter ACCEPT_ALL = connComp -> true; + + private static final int MARK = -2; + private static final int NON_LABEL = -1; + + private final BufferedImage image; + private final int width; + private final int height; + + private final ColorProcessor labels; + + private final int foreground; + + public ConnectedComponentLabeler(BufferedImage image, boolean blackOnWhite) { + if (image.getType() != BufferedImage.TYPE_BYTE_BINARY) { + throw new IllegalArgumentException("not a binary image"); + } + + this.image = image; + this.width = image.getWidth(); + this.height = image.getHeight(); + + this.labels = new ColorProcessor(width + 2, height + 2); + this.labels.setColor(NON_LABEL); + this.labels.fill(); + + if (blackOnWhite) { + foreground = 0xFF000000; + } else { + foreground = 0xFFFFFFFF; + } + } + + public List apply(ConnectedComponentFilter filter) { + final List connectedComponents = apply(); + + return connectedComponents.stream() + .filter(filter::filter) + .collect(Collectors.toList()); + } + + /** + * For more info, see Chang, Chen et al. 2004. + */ + public List apply() { + final ArrayList connectedComponents = new ArrayList<>(); + int label = NON_LABEL + 1; // current label counter (C in the paper) + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + if (isForeground(x, y)) { + // starting point of a new outer contour + if (!isForeground(x, y - 1) && !hasLabel(x, y)) { + labels.set(x, y, label); + + final Polygon outer = trace(x, y, label, true); + + // trace external + final ConnectedComponent connComp = + new ConnectedComponent(label, outer); + connectedComponents.add(connComp); + + // increase C + label++; + } + + // starting point of a new inner contour + if (!isForeground(x, y + 1) && !hasMark(x, y + 1)) { + if (!hasLabel(x, y)) { + // if this pixel doesn't have a label, assign it the + // same label like the pixel to the left + setLabel(x, y, getLabel(x - 1, y)); + } + + final int local = getLabel(x, y); + if (local >= 0) { + final Polygon contour = trace(x, y, local, false); + + connectedComponents.get(local).addInnerContour(contour); + } + } + + // non-labeled pixel + else if (!hasLabel(x, y)) { + // label it with previous pixel's label + setLabel(x, y, getLabel(x - 1, y)); + } + } + } + } + + return connectedComponents; + } + + private Polygon trace(int startX, int startY, int label, boolean isOuter) { + final Polygon contour = new Polygon(); + contour.addPoint(startX, startY); + + final int[] point = new int[]{startX, startY}; + + int pos; + if (isOuter) { + pos = -7; + } else { + pos = -3; + } + + pos = traceNext(point, pos); + + // single/isolated point + if (pos == -1) { + return contour; + } + + final int nextX = point[0], nextY = point[1]; + + boolean equalsStartPoint; + do { + contour.addPoint(point[0], point[1]); + setLabel(point[0], point[1], label); + equalsStartPoint = + point[0] == startX && point[1] == startY; + pos = traceNext(point, pos); + + } while (!equalsStartPoint || point[0] != nextX || point[1] != nextY); + + return contour; + } + + private int traceNext(int[] point, int position) { + if (position < 0) { + position = -position; + } else { + position = (position + 6) % 8; + } + + final int start = position; + + int[] nextPoint = new int[2]; + do { + nextPosition(point, nextPoint, position); + + if (isForeground(nextPoint[0], nextPoint[1])) { + point[0] = nextPoint[0]; + point[1] = nextPoint[1]; + return position; + } else { + setMark(nextPoint[0], nextPoint[1]); + } + + position = (position + 1) % 8; + } while (position != start); + + return -1; + } + + private void nextPosition(int[] point, int[] nextPoint, int position) { + // indexes of the neighboring points of P = (x, y) + // as defined by Chang, Chen et al. 2004: + // + // | 5 6 7 | + // | 4 P 0 | + // | 3 2 1 | + // + + switch (position) { + case 0: // right + nextPoint[0] = point[0] + 1; + nextPoint[1] = point[1]; + break; + case 1: // bottom right + nextPoint[0] = point[0] + 1; + nextPoint[1] = point[1] + 1; + break; + case 2: // bottom + nextPoint[0] = point[0]; + nextPoint[1] = point[1] + 1; + break; + case 3: + nextPoint[0] = point[0] - 1; + nextPoint[1] = point[1] + 1; + break; + case 4: + nextPoint[0] = point[0] - 1; + nextPoint[1] = point[1]; + break; + case 5: + nextPoint[0] = point[0] - 1; + nextPoint[1] = point[1] - 1; + break; + case 6: + nextPoint[0] = point[0]; + nextPoint[1] = point[1] - 1; + break; + case 7: + nextPoint[0] = point[0] + 1; + nextPoint[1] = point[1] - 1; + break; + default: + throw new IllegalArgumentException("invalid position " + + position); + } + } + + private int getPixel(int x, int y) { + return image.getRGB(x, y); + } + + private boolean isForeground(int x, int y) { + if (x < 0 || x >= width || y < 0 || y >= height) { + return false; + } + + return getPixel(x, y) == foreground; + } + + private void setMark(int x, int y) { + setLabel(x, y, MARK); + } + + private boolean hasMark(int x, int y) { + return getLabel(x, y) == MARK; + } + + private void setLabel(int x, int y, int value) { + labels.set(x + 1, y + 1, value); + } + + private int getLabel(int x, int y) { + return labels.get(x + 1, y + 1); + } + + private boolean hasLabel(int x, int y) { + return getLabel(x, y) != NON_LABEL; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/Connectivity.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/Connectivity.java new file mode 100644 index 00000000..87948bb8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/conncomp/Connectivity.java @@ -0,0 +1,5 @@ +package de.vorb.tesseract.tools.preprocessing.conncomp; + +public enum Connectivity { + FOUR, EIGHT +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/BlobSizeFilter.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/BlobSizeFilter.java new file mode 100644 index 00000000..6770bf58 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/BlobSizeFilter.java @@ -0,0 +1,55 @@ +package de.vorb.tesseract.tools.preprocessing.filter; + +import de.vorb.tesseract.tools.preprocessing.conncomp.ConnectedComponent; +import de.vorb.tesseract.tools.preprocessing.conncomp.ConnectedComponentLabeler; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.List; + +public class BlobSizeFilter implements ImageFilter { + private final int minArea; + private final int maxArea; + + public BlobSizeFilter(int minArea, int maxArea) { + this.minArea = minArea; + if (maxArea == 0) { + this.maxArea = Integer.MAX_VALUE; + } else { + this.maxArea = maxArea; + } + } + + public int getMinArea() { + return minArea; + } + + public int getMaxArea() { + return maxArea == Integer.MAX_VALUE ? 0 : maxArea; + } + + @Override + public void filter(BufferedImage image) { + if (image.getType() != BufferedImage.TYPE_BYTE_BINARY) { + throw new IllegalArgumentException("not a binary image"); + } + + final ConnectedComponentLabeler labeler = + new ConnectedComponentLabeler(image, true); + final List connectedComponents = + labeler.apply(); + + // clear the input image + final Graphics2D g2d = image.createGraphics(); + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + + g2d.setColor(Color.BLACK); + connectedComponents.stream() + .filter(connComp -> connComp.getArea() <= maxArea && connComp.getArea() >= minArea) + .forEach(connComp -> connComp.drawOn(g2d)); + + g2d.dispose(); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/ImageFilter.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/ImageFilter.java new file mode 100644 index 00000000..caeb07de --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/filter/ImageFilter.java @@ -0,0 +1,19 @@ +package de.vorb.tesseract.tools.preprocessing.filter; + +import java.awt.image.BufferedImage; + +/** + * Image filter. + * + * @author Paul Vorbach + */ +public interface ImageFilter { + /** + * Filters the image in-situ. + *

+ * Make a copy of the source image if you want to + * + * @param image source image + */ + void filter(BufferedImage image); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/package-info.java b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/package-info.java new file mode 100644 index 00000000..4f95cb07 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/preprocessing/package-info.java @@ -0,0 +1,6 @@ +/** + * Pre-processing tools. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.tools.preprocessing; diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/DefaultRecognitionConsumer.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/DefaultRecognitionConsumer.java new file mode 100644 index 00000000..ab7f2102 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/DefaultRecognitionConsumer.java @@ -0,0 +1,51 @@ +package de.vorb.tesseract.tools.recognition; + +public abstract class DefaultRecognitionConsumer implements RecognitionConsumer { + private RecognitionState state; + + @Override + public void setState(RecognitionState state) { + this.state = state; + } + + @Override + public RecognitionState getState() { + return state; + } + + @Override + public void blockBegin() { + } + + @Override + public void blockEnd() { + } + + @Override + public void paragraphBegin() { + } + + @Override + public void paragraphEnd() { + } + + @Override + public void lineBegin() { + } + + @Override + public void lineEnd() { + } + + @Override + public void wordBegin() { + } + + @Override + public void wordEnd() { + } + + @Override + public void symbol() { + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/PageRecognitionConsumer.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/PageRecognitionConsumer.java new file mode 100644 index 00000000..b9aa6eda --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/PageRecognitionConsumer.java @@ -0,0 +1,89 @@ +package de.vorb.tesseract.tools.recognition; + +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Line; +import de.vorb.tesseract.util.Paragraph; +import de.vorb.tesseract.util.Symbol; +import de.vorb.tesseract.util.Word; + +import org.bytedeco.javacpp.tesseract; + +import java.util.ArrayList; +import java.util.List; + +public abstract class PageRecognitionConsumer extends + DefaultRecognitionConsumer { + + private final List blocks; + private ArrayList blockParagraphs; + private ArrayList paragraphLines; + private ArrayList lineWords; + private ArrayList wordSymbols; + + public PageRecognitionConsumer(List blocks) { + this.blocks = blocks; + } + + @Override + public void blockBegin() { + blockParagraphs = new ArrayList<>(); + } + + @Override + public void blockEnd() { + final int level = tesseract.RIL_BLOCK; + blocks.add(new Block(getState().getBoundingBox(level), + blockParagraphs)); + } + + @Override + public void paragraphBegin() { + paragraphLines = new ArrayList<>(); + } + + @Override + public void paragraphEnd() { + final int level = tesseract.RIL_PARA; + blockParagraphs.add(new Paragraph(getState().getBoundingBox(level), + paragraphLines)); + } + + @Override + public void lineBegin() { + lineWords = new ArrayList<>(); + } + + @Override + public void lineEnd() { + final int level = tesseract.RIL_TEXTLINE; + paragraphLines.add(new Line(getState().getBoundingBox(level), + lineWords, getState().getBaseline(level))); + } + + @Override + public void wordBegin() { + wordSymbols = new ArrayList<>(); + } + + @Override + public void wordEnd() { + final RecognitionState state = getState(); + final int level = tesseract.RIL_WORD; + final Box boundingBox = state.getBoundingBox(level); + lineWords.add(new Word(wordSymbols, boundingBox, + state.getConfidence(level), + state.getBaseline(tesseract.RIL_WORD), + state.getWordFontAttributes())); + } + + @Override + public void symbol() { + final int level = tesseract.RIL_SYMBOL; + wordSymbols.add(new Symbol( + getState().getText(level), + getState().getBoundingBox(level), + getState().getConfidence(level), + getState().getAlternatives())); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionConsumer.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionConsumer.java new file mode 100644 index 00000000..9937b91d --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionConsumer.java @@ -0,0 +1,71 @@ +package de.vorb.tesseract.tools.recognition; + +/** + * Consumes the OCR results of Tesseract. + * + * @author Paul Vorbach + */ +public interface RecognitionConsumer { + /** + * @param state state of the recognition process + */ + void setState(RecognitionState state); + + /** + * @return current state of the recognition process + */ + RecognitionState getState(); + + /** + * Beginning of a block. + */ + void blockBegin(); + + /** + * End of a block. + */ + void blockEnd(); + + /** + * Beginning of a paragraph. + */ + void paragraphBegin(); + + /** + * End of a paragraph. + */ + void paragraphEnd(); + + /** + * Beginning of a text line. + */ + void lineBegin(); + + /** + * End of a text line. + */ + void lineEnd(); + + /** + * Beginning of a word. + */ + void wordBegin(); + + /** + * End of a word. + */ + void wordEnd(); + + /** + * Symbol within a word. + */ + void symbol(); + + /** + * Provides cancellation information for the recognition provider. + * + * @return {@code true} if the task shall be cancelled, {@code false} + * otherwise. + */ + boolean isCancelled(); +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionProducer.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionProducer.java new file mode 100644 index 00000000..f79008d9 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionProducer.java @@ -0,0 +1,138 @@ +package de.vorb.tesseract.tools.recognition; + +import org.bytedeco.javacpp.tesseract; + +import java.io.Closeable; +import java.io.IOException; + +public abstract class RecognitionProducer implements Closeable { + public static final String DEFAULT_TRAINING_FILE = "eng"; + + private tesseract.TessBaseAPI handle; + private String trainingFile = DEFAULT_TRAINING_FILE; + + public RecognitionProducer() { + } + + public RecognitionProducer(String trainingFile) { + setTrainingFile(trainingFile); + } + + public tesseract.TessBaseAPI getHandle() { + return this.handle; + } + + public String getTrainingFile() { + return trainingFile; + } + + public void setTrainingFile(String trainingFile) { + this.trainingFile = trainingFile; + } + + protected void setHandle(tesseract.TessBaseAPI handle) { + this.handle = handle; + } + + public abstract void init() throws IOException; + + public abstract void reset() throws IOException; + + public abstract void close() throws IOException; + + @SuppressWarnings("unchecked") + public void recognize(RecognitionConsumer consumer) { + // text recognition + tesseract.TessBaseAPIRecognize(getHandle(), null); + + // get the result iterator + final tesseract.ResultIterator resultIt = + tesseract.TessBaseAPIGetIterator(getHandle()); + + // get the page iterator + final tesseract.PageIterator pageIt = + tesseract.TessResultIteratorGetPageIterator(resultIt); + + // set the recognition state + consumer.setState(new RecognitionState(handle, resultIt, pageIt)); + + boolean inWord = false; + + do { + // beginning of a symbol + if (tesseract.TessPageIteratorIsAtBeginningOf(pageIt, tesseract.RIL_SYMBOL)) { + + // beginning of a word + if (tesseract.TessPageIteratorIsAtBeginningOf(pageIt, tesseract.RIL_WORD)) { + + // beginning of a text line + if (tesseract.TessPageIteratorIsAtBeginningOf(pageIt, tesseract.RIL_TEXTLINE)) { + + // beginning of a paragraph + if (tesseract.TessPageIteratorIsAtBeginningOf(pageIt, tesseract.RIL_PARA)) { + + // beginning of a block + if (tesseract.TessPageIteratorIsAtBeginningOf(pageIt, tesseract.RIL_BLOCK)) { + consumer.blockBegin(); + + // handle cancellation + if (consumer.isCancelled()) { + + // end block + consumer.blockEnd(); + + // stop iteration + break; + } + } + + consumer.paragraphBegin(); + } + + consumer.lineBegin(); + } + + consumer.wordBegin(); + + inWord = true; + } + + consumer.symbol(); + } + + if (!inWord) { + continue; + } + + // last symbol in word + if (tesseract.TessPageIteratorIsAtFinalElement(pageIt, tesseract.RIL_WORD, tesseract.RIL_SYMBOL)) { + + consumer.wordEnd(); + + inWord = false; + + // last word in line + if (tesseract.TessPageIteratorIsAtFinalElement(pageIt, tesseract.RIL_TEXTLINE, tesseract.RIL_WORD)) { + + consumer.lineEnd(); + + // last line in paragraph + if (tesseract.TessPageIteratorIsAtFinalElement(pageIt, tesseract.RIL_PARA, + tesseract.RIL_TEXTLINE)) { + + consumer.paragraphEnd(); + + // last paragraph in a block + if (tesseract.TessPageIteratorIsAtFinalElement(pageIt, tesseract.RIL_BLOCK, + tesseract.RIL_PARA)) { + consumer.blockEnd(); + } + } + } + } + } while (tesseract.TessPageIteratorNext(pageIt, tesseract.RIL_SYMBOL)); // next symbol + + // tesseract.TessResultIteratorDelete(resultIt); + // tesseract.TessPageIteratorDelete(pageIt); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionState.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionState.java new file mode 100644 index 00000000..5bef6426 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/RecognitionState.java @@ -0,0 +1,202 @@ +package de.vorb.tesseract.tools.recognition; + +import de.vorb.tesseract.util.AlternativeChoice; +import de.vorb.tesseract.util.Baseline; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.FontAttributes; + +import org.bytedeco.javacpp.BoolPointer; +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.IntPointer; +import org.bytedeco.javacpp.lept; +import org.bytedeco.javacpp.tesseract; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class RecognitionState { + + private final tesseract.TessBaseAPI apiHandle; + private final tesseract.ResultIterator resultIt; + private final tesseract.PageIterator pageIt; + + public RecognitionState(tesseract.TessBaseAPI apiHandle, tesseract.ResultIterator resultIt, + tesseract.PageIterator pageIt) { + this.apiHandle = apiHandle; + this.resultIt = resultIt; + this.pageIt = pageIt; + } + + /** + * Get the bounding box at the given iterator level. + * + * @param level level of the requested box + * @return requested box + */ + public Box getBoundingBox(int level) { + try (final IntPointer left = new IntPointer(1); + final IntPointer top = new IntPointer(1); + final IntPointer right = new IntPointer(1); + final IntPointer bottom = new IntPointer(1)) { + + tesseract.TessPageIteratorBoundingBox(pageIt, level, left, top, right, bottom); + + final int x = left.get(); + final int y = top.get(); + final int width = right.get() - x; + final int height = bottom.get() - y; + + return new Box(x, y, width, height); + } + } + + /** + * Get the text content at the given iterator level. + * + * @param level level of the requested text + * @return requested text + */ + public String getText(int level) { + final BytePointer pText = tesseract.TessResultIteratorGetUTF8Text(resultIt, level); + final String text = new String(pText.getStringBytes(), StandardCharsets.UTF_8); + tesseract.TessDeleteText(pText); + return text; + } + + /** + * Get the baseline information of the given iterator level. + * + * @param level level of the requested baseline + * @return baseline + */ + public Baseline getBaseline(int level) { + try (final IntPointer x1 = new IntPointer(1); + final IntPointer y1 = new IntPointer(1); + final IntPointer x2 = new IntPointer(1); + final IntPointer y2 = new IntPointer(1)) { + + tesseract.TessPageIteratorBaseline(pageIt, level, x1, y1, x2, y2); + + final int width = x2.get() - x1.get(); + final float height = y2.get() - y1.get(); + final float slope = height / width; + + final Box boundingBox = getBoundingBox(tesseract.RIL_WORD); + final int yOffset = boundingBox.getY() + boundingBox.getHeight() - y1.get(); + + return new Baseline(yOffset, slope); + } + } + + /** + * Get the confidence of the given iterator level. + * + * @param level level of the requested confidence + * @return recognition confidence + */ + public float getConfidence(int level) { + return tesseract.TessResultIteratorConfidence(resultIt, level); + } + + public List getAlternatives() { + final List alternatives = new ArrayList<>(); + + final tesseract.ChoiceIterator choiceIt = tesseract.TessResultIteratorGetChoiceIterator(resultIt); + + // pull out all choices + do { + final BytePointer choice = tesseract.TessChoiceIteratorGetUTF8Text(choiceIt); + final float conf = tesseract.TessChoiceIteratorConfidence(choiceIt); + + alternatives.add(new AlternativeChoice(new String(choice.getStringBytes(), StandardCharsets.UTF_8), conf)); + } while (tesseract.TessChoiceIteratorNext(choiceIt)); + + return alternatives; + } + + private void getSymbolFeatures(lept.PIX image) { + + try (final IntPointer left = new IntPointer(1); + final IntPointer top = new IntPointer(1); + final IntPointer numFeatures = new IntPointer(1); + final IntPointer featOutlineIndex = new IntPointer(1); + final tesseract.INT_FEATURE_STRUCT intFeatures = new tesseract.INT_FEATURE_STRUCT( + new BytePointer(4 * 512))) { + + final lept.PIX pix = tesseract.TessPageIteratorGetImage(pageIt, tesseract.RIL_SYMBOL, 1, image, left, top); + final tesseract.TBLOB blob = tesseract.TessMakeTBLOB(pix); + + tesseract.TessBaseAPIGetFeaturesForBlob(apiHandle, blob, intFeatures, numFeatures, featOutlineIndex); + } + } + + /** + * @return font attributes for the current word. + */ + public FontAttributes getWordFontAttributes() { + + try (final BoolPointer isBold = new BoolPointer(1); + final BoolPointer isItalic = new BoolPointer(1); + final BoolPointer isUnderlined = new BoolPointer(1); + final BoolPointer isMonospace = new BoolPointer(1); + final BoolPointer isSerif = new BoolPointer(1); + final BoolPointer isSmallCaps = new BoolPointer(1); + final IntPointer fontSize = new IntPointer(1); + final IntPointer fontID = new IntPointer(1)) { + + // set values + tesseract.TessResultIteratorWordFontAttributes(resultIt, isBold, isItalic, isUnderlined, isMonospace, + isSerif, + isSmallCaps, fontSize, fontID); + + // build and return FontAttributes + + return new FontAttributes.Builder() + .bold(isBold.get()) + .italic(isItalic.get()) + .underlined(isUnderlined.get()) + .monospace(isMonospace.get()) + .serif(isSerif.get()) + .smallCaps(isSmallCaps.get()) + .size(fontSize.get()) + .fontID(fontID.get()) + .build(); + } + } + + /** + * @return true if word exists in dictionary + */ + public boolean isWordFromDictionary() { + return tesseract.TessResultIteratorWordIsFromDictionary(resultIt); + } + + /** + * @return true if word is numeric + */ + public boolean isWordNumeric() { + return tesseract.TessResultIteratorWordIsNumeric(resultIt); + } + + /** + * @return true if current symbol is a drop cap + */ + public boolean isSymbolDropCap() { + return tesseract.TessResultIteratorSymbolIsDropcap(resultIt); + } + + /** + * @return true if current symbol is subscript + */ + public boolean isSymbolSubscript() { + return tesseract.TessResultIteratorSymbolIsSubscript(resultIt); + } + + /** + * @return true if current symbol is superscript + */ + public boolean isSymbolSuperscript() { + return tesseract.TessResultIteratorSymbolIsSuperscript(resultIt); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/recognition/package-info.java b/tools/src/main/java/de/vorb/tesseract/tools/recognition/package-info.java new file mode 100644 index 00000000..8eb8d9ef --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/recognition/package-info.java @@ -0,0 +1,6 @@ +/** + * Recognition related tools. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.tools.recognition; diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/BoxFiles.java b/tools/src/main/java/de/vorb/tesseract/tools/training/BoxFiles.java new file mode 100644 index 00000000..7c0b9647 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/BoxFiles.java @@ -0,0 +1,91 @@ +package de.vorb.tesseract.tools.training; + +import de.vorb.tesseract.util.Block; +import de.vorb.tesseract.util.Box; +import de.vorb.tesseract.util.Line; +import de.vorb.tesseract.util.Page; +import de.vorb.tesseract.util.Paragraph; +import de.vorb.tesseract.util.Symbol; +import de.vorb.tesseract.util.Word; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.List; + +/** + * Methods for creating box files. + * + * @author Paul Vorbach + */ +public final class BoxFiles { + + private BoxFiles() {} + + /** + * Writes a single page to the given Writer in the box file format. + * + * @param out + * @param page + * @param pageIndex + * @throws IOException + */ + public static void writePageTo(Writer out, Page page, int pageIndex) + throws IOException { + for (final Block block : page.getBlocks()) { + for (final Paragraph para : block.getParagraphs()) { + for (final Line line : para.getLines()) { + for (final Word word : line.getWords()) { + for (final Symbol symbol : word.getSymbols()) { + final Box box = symbol.getBoundingBox(); + + // line format: text x y width height index + out.append(symbol.getText()).append(' ').append( + String.valueOf(box.getX())).append(' ').append( + String.valueOf(box.getY())).append(' ').append( + String.valueOf(box.getWidth())).append(' ').append( + String.valueOf(box.getHeight())).append(' ').append( + String.valueOf(pageIndex)).append('\n'); + } + } + } + } + } + } + + /** + * Writes multiple pages to the given Writer in the box file format. + * + * @param out + * @param pages + * @throws IOException + */ + public static void writeTo(Writer out, List pages) + throws IOException { + int i = 0; + for (final Page page : pages) { + writePageTo(out, page, i++); + } + } + + /** + * Creates or overwrites the given file with multiple pages in the box file + * format. + * + * @param file + * @param pages + * @throws IOException + */ + public static void writeTo(Path file, List pages) throws IOException { + Writer out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(file.toFile()), Charset.forName("UTF-8"))); + + writeTo(out, pages); + + out.close(); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/Char.java b/tools/src/main/java/de/vorb/tesseract/tools/training/Char.java new file mode 100644 index 00000000..60b6eb15 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/Char.java @@ -0,0 +1,79 @@ +package de.vorb.tesseract.tools.training; + + +public class Char { + private final String text; + private final CharacterProperties props; + private final CharacterDimensions dims; + private final String script; + private final int otherCase; + private final int dir; + private final int mirror; + private final String normed; + + public Char(String text, CharacterProperties properties, + CharacterDimensions dimensions, String script, int otherCase, + int direction, + int mirror, String normed) { + this.text = text; + this.props = properties; + this.dims = dimensions; + this.script = script; + this.otherCase = otherCase; + this.dir = direction; + this.mirror = mirror; + this.normed = normed; + } + + public Char(String text, CharacterProperties properties, String script, int otherCase) { + this(text, properties, CharacterDimensions.DEFAULT, script, otherCase, 0, 0, ""); + } + + public String getText() { + return text; + } + + public CharacterProperties getProperties() { + return props; + } + + public CharacterDimensions getDimensions() { + return dims; + } + + public String getScript() { + return script; + } + + public int getOtherCase() { + return otherCase; + } + + public int getDirection() { + return dir; + } + + public int getMirror() { + return mirror; + } + + public String getNormed() { + return normed; + } + + @Override + public String toString() { + if (text.equals(" ")) { + return "NULL 0 NULL 0"; + } else { + return String.format( + "%s %d %d,%d,%d,%d,%d,%d,%d,%d,%d,%d %s %d %d %d %s", + text, props.toByteCode(), dims.getMinBottom(), + dims.getMaxBottom(), dims.getMinTop(), dims.getMaxTop(), + dims.getMinWidth(), dims.getMaxWidth(), + dims.getMinBearing(), dims.getMaxBearing(), + dims.getMinAdvance(), dims.getMaxAdvance(), script, + otherCase, dir, mirror, normed); + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterDimensions.java b/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterDimensions.java new file mode 100644 index 00000000..12e38613 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterDimensions.java @@ -0,0 +1,72 @@ +package de.vorb.tesseract.tools.training; + +public class CharacterDimensions { + public static final CharacterDimensions DEFAULT = new CharacterDimensions( + 0, 255, 0, 255, 0, 32767, 0, 32767, 0, 32767); + + private final int minBottom; + private final int maxBottom; + private final int minTop; + private final int maxTop; + private final int minWidth; + private final int maxWidth; + private final int minBearing; + private final int maxBearing; + private final int minAdvance; + private final int maxAdvance; + + public CharacterDimensions(int minBottom, int maxBottom, int minTop, + int maxTop, int minWidth, int maxWidth, int minBearing, + int maxBearing, int minAdvance, int maxAdvance) { + this.minBottom = minBottom; + this.maxBottom = maxBottom; + this.minTop = minTop; + this.maxTop = maxTop; + this.minWidth = minWidth; + this.maxWidth = maxWidth; + this.minBearing = minBearing; + this.maxBearing = maxBearing; + this.minAdvance = minAdvance; + this.maxAdvance = maxAdvance; + } + + public int getMinBottom() { + return minBottom; + } + + public int getMaxBottom() { + return maxBottom; + } + + public int getMinTop() { + return minTop; + } + + public int getMaxTop() { + return maxTop; + } + + public int getMinWidth() { + return minWidth; + } + + public int getMaxWidth() { + return maxWidth; + } + + public int getMinBearing() { + return minBearing; + } + + public int getMaxBearing() { + return maxBearing; + } + + public int getMinAdvance() { + return minAdvance; + } + + public int getMaxAdvance() { + return maxAdvance; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterProperties.java b/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterProperties.java new file mode 100644 index 00000000..7f9a74f0 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/CharacterProperties.java @@ -0,0 +1,126 @@ +package de.vorb.tesseract.tools.training; + +import java.util.regex.Pattern; + +public class CharacterProperties { + private final static int BIT_ALPHA = 0; + private final static int BIT_LOWER = 1; + private final static int BIT_UPPER = 2; + private final static int BIT_DIGIT = 3; + private final static int BIT_PUNCTUATION = 4; + + private final byte code; + + private CharacterProperties(byte code) { + this.code = code; + } + + private boolean getBit(int bit) { + return ((code >> bit) & 1) == 1; + } + + public CharacterProperties(boolean isAlpha, boolean isDigit, + boolean isUpper, boolean isLower, boolean isPunctuation) { + byte code = 0; + code |= isAlpha ? 1 : 0; + code |= isLower ? 2 : 0; + code |= isUpper ? 4 : 0; + code |= isDigit ? 8 : 0; + code |= isPunctuation ? 16 : 0; + this.code = code; + } + + public static CharacterProperties forByteCode(byte code) { + return new CharacterProperties(code); + } + + public static CharacterProperties forHexString(String hexString) { + return new CharacterProperties((byte) Integer.parseInt(hexString, 16)); + } + + private final static Pattern ALPHA = Pattern.compile("\\p{Alpha}+", + Pattern.UNICODE_CHARACTER_CLASS); + private final static Pattern DIGIT = Pattern.compile("\\p{Digit}+", + Pattern.UNICODE_CHARACTER_CLASS); + private final static Pattern UPPER = Pattern.compile("\\p{Upper}+", + Pattern.UNICODE_CHARACTER_CLASS); + private final static Pattern LOWER = Pattern.compile("\\p{Lower}+", + Pattern.UNICODE_CHARACTER_CLASS); + private final static Pattern PUNCTUATION = Pattern.compile("\\p{Punct}+", + Pattern.UNICODE_CHARACTER_CLASS); + + public static CharacterProperties forCharacter(char c) { + return forString("" + c); + } + + public static CharacterProperties forString(String cs) { + final boolean isAlpha = ALPHA.matcher(cs).matches(); + final boolean isDigit = DIGIT.matcher(cs).matches(); + final boolean isUpper = UPPER.matcher(cs).matches(); + final boolean isLower = LOWER.matcher(cs).matches(); + final boolean isPunctuation = PUNCTUATION.matcher(cs).matches(); + + return new CharacterProperties(isAlpha, isDigit, isUpper, isLower, + isPunctuation); + } + + public boolean isAlpha() { + return getBit(BIT_ALPHA); + } + + public boolean isLower() { + return getBit(BIT_LOWER); + } + + public boolean isUpper() { + return getBit(BIT_UPPER); + } + + public boolean isDigit() { + return getBit(BIT_DIGIT); + } + + public boolean isPunctuation() { + return getBit(BIT_PUNCTUATION); + } + + public byte toByteCode() { + return code; + } + + public String toHexString() { + return Integer.toHexString(code); + } + + @Override + public String toString() { + return Integer.toBinaryString(code); + } + + @Override + public int hashCode() { + return toByteCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (!(obj instanceof CharacterProperties)) { + return false; + } + CharacterProperties other = (CharacterProperties) obj; + if (isAlpha() != other.isAlpha()) { + return false; + } else if (isDigit() != other.isDigit()) { + return false; + } else if (isLower() != other.isLower()) { + return false; + } else if (isPunctuation() != other.isPunctuation()) { + return false; + } + return isUpper() == other.isUpper(); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/ClassPruner.java b/tools/src/main/java/de/vorb/tesseract/tools/training/ClassPruner.java new file mode 100644 index 00000000..63903b8b --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/ClassPruner.java @@ -0,0 +1,49 @@ +package de.vorb.tesseract.tools.training; + +import java.io.IOException; + +import static de.vorb.tesseract.tools.training.IntTemplates.NUM_CP_BUCKETS; +import static de.vorb.tesseract.tools.training.IntTemplates.WERDS_PER_CP_VECTOR; + +public class ClassPruner { + private final int[][][][] p; + + private ClassPruner(int[][][][] p) { + this.p = p; + } + + public long get(int x, int y, int z, int w) { + return p[x][y][z][w] & 0xFFFF_FFFFL; + } + + public void set(int x, int y, int z, int w, long value) { + p[x][y][z][w] = (int) value; + } + + public static ClassPruner readFromBuffer(InputBuffer buf) + throws IOException { + final int[][][][] p = + new int[NUM_CP_BUCKETS][NUM_CP_BUCKETS][NUM_CP_BUCKETS][WERDS_PER_CP_VECTOR]; + + // read the class pruners + int x, y, z, w; + for (x = 0; x < NUM_CP_BUCKETS; x++) { + for (y = 0; y < NUM_CP_BUCKETS; y++) { + for (z = 0; z < NUM_CP_BUCKETS; z++) { + for (w = 0; w < WERDS_PER_CP_VECTOR; w++) { + if (!buf.readInt()) { + throw new IOException( + String.format( + "Not enough class pruners (x = %d, y = %d, z = %d, w = %d)", + x, y, z, w)); + } + + p[x][y][z][w] = buf.getInt(); + } + } + } + } + + return new ClassPruner(p); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/InputBuffer.java b/tools/src/main/java/de/vorb/tesseract/tools/training/InputBuffer.java new file mode 100644 index 00000000..cf5bea43 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/InputBuffer.java @@ -0,0 +1,148 @@ +package de.vorb.tesseract.tools.training; + +import java.io.BufferedInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +public class InputBuffer implements Closeable, AutoCloseable { + protected final BufferedInputStream in; + protected long buf; + protected boolean littleEndian = true; + + protected InputBuffer(BufferedInputStream in) { + this.in = in; + } + + protected InputBuffer(InputStream in, int capacity) { + this(new BufferedInputStream(in, capacity)); + } + + public ByteOrder getByteOrder() { + return littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + } + + public void setByteOrder(ByteOrder order) { + littleEndian = order == ByteOrder.LITTLE_ENDIAN; + } + + public boolean readByte() throws IOException { + buf = in.read(); + + return buf >= 0L; + } + + public byte getByte() { + return (byte) buf; + } + + public boolean readShort() throws IOException { + if (littleEndian) { + buf = (in.read() << 8) // 1st byte + | in.read(); // 2nd byte + } else { + buf = in.read() // 1st byte + | (in.read() << 8); // 2nd byte + } + + return buf >= 0L; + } + + public short getShort() { + return (short) buf; + } + + public boolean readInt() throws IOException { + if (littleEndian) { + buf = (((long) in.read()) << 24) // 1st byte + | (((long) in.read()) << 16) // 2nd byte + | (((long) in.read()) << 8) // 3rd byte + | ((long) in.read()); // 4th byte + } else { + buf = ((long) in.read()) // 1st byte + | (((long) in.read()) << 8) // 2nd byte + | (((long) in.read()) << 16) // 3rd byte + | (((long) in.read()) << 24); // 4th byte + } + + return buf >= 0L; + } + + public int getInt() { + return (int) buf; + } + + public boolean readLong() throws IOException { + if (littleEndian) { + buf = (((long) in.read()) << 56) // 1st byte + | (((long) in.read()) << 48) // 2nd byte + | (((long) in.read()) << 40) // 3rd byte + | (((long) in.read()) << 32) // 4th byte + | (((long) in.read()) << 24) // 5th byte + | (((long) in.read()) << 16) // 6th byte + | (((long) in.read()) << 8) // 7th byte + | ((long) in.read()); // 8th byte + } else { + buf = ((long) in.read()) // 1st byte + | (((long) in.read()) << 8) // 2nd byte + | (((long) in.read()) << 16) // 3rd byte + | (((long) in.read()) << 24) // 4th byte + | (((long) in.read()) << 32) // 5th byte + | (((long) in.read()) << 40) // 6th byte + | (((long) in.read()) << 48) // 7th byte + | (((long) in.read()) << 56); // 8th byte + } + + return buf >= 0L; + } + + public long getLong() { + return buf; + } + + public boolean readChar() throws IOException { + return readShort(); + } + + public char getChar() { + return (char) buf; + } + + public boolean readFloat() throws IOException { + return readInt(); + } + + public float getFloat() { + return Float.intBitsToFloat((int) buf); + } + + public boolean readDouble() throws IOException { + return readLong(); + } + + public double getDouble() { + return Double.longBitsToDouble(buf); + } + + public int readBuffer(byte[] buf) throws IOException { + return in.read(buf); + } + + public int readBuffer(byte[] buf, int off, int len) throws IOException { + return in.read(buf, off, len); + } + + @Override + public void close() throws IOException { + in.close(); + } + + public static InputBuffer allocate(BufferedInputStream in) { + return new InputBuffer(in); + } + + public static InputBuffer allocate(InputStream in, int capacity) { + return new InputBuffer(in, capacity); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/IntClass.java b/tools/src/main/java/de/vorb/tesseract/tools/training/IntClass.java new file mode 100644 index 00000000..c8206be8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/IntClass.java @@ -0,0 +1,147 @@ +package de.vorb.tesseract.tools.training; + +import de.vorb.tesseract.util.feat.Feature4D; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static de.vorb.tesseract.tools.training.IntTemplates.PROTOS_PER_PROTO_SET; +import static de.vorb.tesseract.tools.training.IntTemplates.WERDS_PER_CONFIG_VEC; + +public class IntClass { + private final int numProtos; + private final ArrayList protoSets; + private final byte[] protoLengths; + private final short[] configLengths; + private final int fontSetId; + + private IntClass(int numProtos, ArrayList protoSets, + byte[] protoLengths, + short[] configLengths, int fontSetId) { + this.numProtos = numProtos; + this.protoSets = protoSets; + this.protoLengths = protoLengths; + this.configLengths = configLengths; + this.fontSetId = fontSetId; + } + + public int getNumProtos() { + return numProtos; + } + + public List getProtoSets() { + return Collections.unmodifiableList(protoSets); + } + + public byte[] getProtoLengths() { + return protoLengths; + } + + public short[] getConfigLengths() { + return configLengths; + } + + public int getFontSetId() { + return fontSetId; + } + + public static IntClass readFromBuffer(InputBuffer buf) + throws IOException { + + // see intproto.cpp@966 + if (!buf.readShort()) { + throw new IOException("invalid int class header"); + } + final int numProtos = buf.getShort() & 0xFFFF; + + if (!buf.readByte()) { + throw new IOException("invalid int class header"); + } + final int numProtoSets = buf.getByte() & 0xFF; + + if (!buf.readByte()) { + throw new IOException("invalid int class header"); + } + final int numConfigs = buf.getByte() & 0xFF; + + // read config lengths + final short[] configLengths = new short[numConfigs]; + for (int i = 0; i < numConfigs; i++) { + if (!buf.readShort()) { + throw new IOException("not enough config lengths"); + } + + configLengths[i] = buf.getShort(); + } + + // read proto lengths + final byte[] protoLengths = new byte[numProtoSets + * PROTOS_PER_PROTO_SET]; + for (int i = 0; i < protoLengths.length; i++) { + if (!buf.readByte()) { + throw new IOException("not enough proto lengths"); + } + + protoLengths[i] = buf.getByte(); + } + + // read proto sets + final ArrayList protoSets = new ArrayList<>(numProtoSets); + for (int i = 0; i < numProtoSets; i++) { + // read pruner + final int[][][] protoPruner = new int[3][64][2]; + for (int x = 0; x < 3; x++) { + for (int y = 0; y < 64; y++) { + for (int z = 0; z < 2; z++) { + if (!buf.readInt()) { + throw new IOException("not enough proto pruners"); + } + + protoPruner[x][y][z] = buf.getInt(); + } + } + } + + final ArrayList protos = + new ArrayList<>(PROTOS_PER_PROTO_SET); + for (int x = 0; x < PROTOS_PER_PROTO_SET; x++) { + // get prototype information + if (!buf.readByte()) + throw new IOException("not enough protos"); + final byte a = buf.getByte(); + + if (!buf.readByte()) + throw new IOException("not enough protos"); + final byte b = buf.getByte(); + + if (!buf.readByte()) + throw new IOException("not enough protos"); + final byte c = buf.getByte(); + + if (!buf.readByte()) + throw new IOException("not enough protos"); + final byte angle = buf.getByte(); + + final int[] configs = new int[WERDS_PER_CONFIG_VEC]; + for (int y = 0; y < WERDS_PER_CONFIG_VEC; y++) { + if (!buf.readInt()) + throw new IOException("not enough prototype configs"); + configs[y] = buf.getInt(); + } + + protos.add(new Feature4D(a, b, c, angle, configs)); + } + + protoSets.add(new ProtoSet(protoPruner, protos)); + } + + if (!buf.readInt()) + throw new IOException("missing font set id"); + final int fontSetId = buf.getInt(); + + return new IntClass(numProtos, protoSets, protoLengths, configLengths, + fontSetId); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/IntTemplates.java b/tools/src/main/java/de/vorb/tesseract/tools/training/IntTemplates.java new file mode 100644 index 00000000..93406f60 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/IntTemplates.java @@ -0,0 +1,91 @@ +package de.vorb.tesseract.tools.training; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class IntTemplates { + public static final float PICO_FEATURE_LENGTH = 0.05f; + public static final float PROTO_PRUNER_SCALE = 4.0f; + public static final int INT_CHAR_NORM_RANGE = 256; + + public static final int MAX_NUM_CONFIGS = 64; + public static final int NUM_CP_BUCKETS = 24; + public static final int NUM_PP_BUCKETS = 64; + public static final int CLASSES_PER_CP = 32; + public static final int NUM_BITS_PER_CLASS = 2; + public static final int BITS_PER_WERD = 32; + public static final int BITS_PER_CP_VECTOR = + CLASSES_PER_CP * NUM_BITS_PER_CLASS; + public static final int WERDS_PER_CP_VECTOR = + BITS_PER_CP_VECTOR / BITS_PER_WERD; + public static final int PROTOS_PER_PROTO_SET = 64; + public static final int WERDS_PER_CONFIG_VEC = + (MAX_NUM_CONFIGS + BITS_PER_WERD - 1) / BITS_PER_WERD; + + private final ArrayList classes; + private final ArrayList pruners; + + private IntTemplates(ArrayList classes, + ArrayList pruners) { + this.classes = classes; + this.pruners = pruners; + } + + public List getClasses() { + return Collections.unmodifiableList(classes); + } + + public List getClassPruners() { + return Collections.unmodifiableList(pruners); + } + + public static IntTemplates readFrom(InputBuffer buf) + throws IOException { + if (!buf.readInt()) + throw new IOException("invalid header"); + // only needed by older formats + @SuppressWarnings({"unused", "UnusedAssignment"}) + final int unicharsetSize = buf.getInt(); + + if (!buf.readInt()) + throw new IOException("invalid header"); + int numClasses = buf.getInt(); + + if (!buf.readInt()) + throw new IOException("invalid header"); + final int numPruners = buf.getInt(); + + final int versionId; + if (numClasses < 0) { + // this file has a version id! + versionId = -numClasses; + + if (!buf.readInt()) + throw new IOException("invalid header"); + numClasses = buf.getInt(); + } else { + versionId = 0; + } + + if (versionId < 4) { + throw new IOException(String.format( + "unsupported inttemp format version '%d'", versionId)); + } + + // read pruners + final ArrayList pruners = new ArrayList<>(numPruners); + for (int i = 0; i < numPruners; i++) { + pruners.add(ClassPruner.readFromBuffer(buf)); + } + + // read classes + final ArrayList classes = new ArrayList<>(numClasses); + for (int i = 0; i < numClasses; i++) { + classes.add(IntClass.readFromBuffer(buf)); + } + + return new IntTemplates(classes, pruners); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/NormProtos.java b/tools/src/main/java/de/vorb/tesseract/tools/training/NormProtos.java new file mode 100644 index 00000000..1ef4ea48 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/NormProtos.java @@ -0,0 +1,5 @@ +package de.vorb.tesseract.tools.training; + +public class NormProtos { + +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/ProtoSet.java b/tools/src/main/java/de/vorb/tesseract/tools/training/ProtoSet.java new file mode 100644 index 00000000..d59cd20d --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/ProtoSet.java @@ -0,0 +1,25 @@ +package de.vorb.tesseract.tools.training; + +import de.vorb.tesseract.util.feat.Feature4D; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ProtoSet { + private final int[][][] pruner; + private final ArrayList protos; + + public ProtoSet(int[][][] pruner, ArrayList protos) { + this.pruner = pruner; + this.protos = protos; + } + + public long getPruner(int x, int y, int z) { + return pruner[x][y][z] & 0xFFFF_FFFFL; + } + + public List getProtos() { + return Collections.unmodifiableList(protos); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/Shape.java b/tools/src/main/java/de/vorb/tesseract/tools/training/Shape.java new file mode 100644 index 00000000..839794da --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/Shape.java @@ -0,0 +1,42 @@ +package de.vorb.tesseract.tools.training; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Shape { + private final boolean sorted; + private final ArrayList uaf; + + public Shape(boolean sorted, ArrayList uaf) { + this.sorted = sorted; + this.uaf = uaf; + } + + public boolean isSorted() { + return sorted; + } + + public List getUnicharAndFonts() { + return Collections.unmodifiableList(uaf); + } + + public static Shape readFrom(InputBuffer buf) throws IOException { + if (!buf.readByte()) + throw new IOException("invalid input format"); + final boolean sorted = buf.getByte() != 0; + + if (!buf.readInt()) + throw new IOException("invalid input format"); + final int size = buf.getInt(); + + final ArrayList uaf = new ArrayList<>(size); + // read data + for (int i = 0; i < size; i++) { + uaf.add(UnicharAndFonts.readFrom(buf)); + } + + return new Shape(sorted, uaf); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataManager.java b/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataManager.java new file mode 100644 index 00000000..32f9b63d --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataManager.java @@ -0,0 +1,36 @@ +package de.vorb.tesseract.tools.training; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TessdataManager { + + private TessdataManager() {} + + private static final String COMMAND = "combine_tessdata"; + + public static void extract(Path tessdataFile, Path pathPrefix) + throws IOException { + if (!Files.isDirectory(pathPrefix.getParent())) { + throw new IOException("non-existing destination directory"); + } + + if (!Files.isWritable(pathPrefix.getParent())) { + throw new IOException("cannot write to destination directory"); + } + + final Process proc = new ProcessBuilder(COMMAND, "-u", + tessdataFile.toString(), pathPrefix.toString()).start(); + + try { + proc.waitFor(); + } catch (InterruptedException e) { + throw new IOException("extraction failed"); + } + + if (proc.exitValue() != 0) { + throw new IOException("extraction failed"); + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataType.java b/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataType.java new file mode 100644 index 00000000..5b7cbd3e --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/TessdataType.java @@ -0,0 +1,58 @@ +package de.vorb.tesseract.tools.training; + +public enum TessdataType { + LANG_CONFIG, // 0 + UNICHARSET, // 1 + AMBIGS, // 2 + INTTEMP, // 3 + PFFMTABLE, // 4 + NORMPROTO, // 5 + PUNC_DAWG, // 6 + SYSTEM_DAWG, // 7 + NUMBER_DAWG, // 8 + FREQ_DAWG, // 9 + FIXED_LENGTH_DAWGS, // 10 // deprecated + CUBE_UNICHARSET, // 11 + CUBE_SYSTEM_DAWG, // 12 + SHAPE_TABLE, // 13 + BIGRAM_DAWG, // 14 + UNAMBIG_DAWG, // 15 + PARAMS_MODEL; // 16 + + private static TessdataType[] values = TessdataType.values(); + + /** + * @param ord ordinal value + * @return corresponding type. + */ + public static TessdataType forOrdinal(int ord) { + if (ord < 0 || ord > 16) + throw new IllegalArgumentException("ordinal value out of range"); + + return values[ord]; + } + + public static int size() { + return values.length; + } + + /** + * @param type Tessdata type. + * @return true if the type has got a binary encoding. For + * UTF-8 encoded types, it returns false. + */ + public static boolean isBinary(TessdataType type) { + switch (type) { + case LANG_CONFIG: + case UNICHARSET: + case AMBIGS: + case PFFMTABLE: + case NORMPROTO: + case CUBE_UNICHARSET: + case PARAMS_MODEL: + return false; + default: + return true; + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/UnicharAndFonts.java b/tools/src/main/java/de/vorb/tesseract/tools/training/UnicharAndFonts.java new file mode 100644 index 00000000..a79c9165 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/UnicharAndFonts.java @@ -0,0 +1,41 @@ +package de.vorb.tesseract.tools.training; + +import java.io.IOException; + +public class UnicharAndFonts { + private final int unicharId; + private final int[] fontIds; + + public UnicharAndFonts(int unicharId, int[] fontIds) { + this.unicharId = unicharId; + this.fontIds = fontIds; + } + + public int getUnicharId() { + return unicharId; + } + + public int[] getFontIds() { + return fontIds; + } + + public static UnicharAndFonts readFrom(InputBuffer buf) throws IOException { + if (!buf.readInt()) + throw new IOException("invalid input format"); + final int unicharId = buf.getInt(); + + if (!buf.readInt()) + throw new IOException("invalid input format"); + final int numOfFonts = buf.getInt(); + + final int[] fontIds = new int[numOfFonts]; + for (int i = 0; i < numOfFonts; i++) { + if (!buf.readInt()) + throw new IOException("invalid input format"); + final int fontId = buf.getInt(); + fontIds[i] = fontId; + } + + return new UnicharAndFonts(unicharId, fontIds); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/Unicharset.java b/tools/src/main/java/de/vorb/tesseract/tools/training/Unicharset.java new file mode 100644 index 00000000..42b9e3b8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/Unicharset.java @@ -0,0 +1,74 @@ +package de.vorb.tesseract.tools.training; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class Unicharset { + private static final Pattern LONG_LINE = + Pattern.compile("^(\\S+) (\\d+) (\\d+),(\\d+),(\\d+),(\\d+),(\\d+)," + + "(\\d+),(\\d+),(\\d+),(\\d+),(\\d+) (\\S+) (\\d+) (\\d+) " + + "(\\d+) (.+)$"); + private static final Pattern FIRST_LINE = + Pattern.compile("^NULL 0 NULL 0$"); + private static final Pattern DELIM = Pattern.compile("\\s+|,"); + + private List characters; + + public Unicharset(List charset) { + this.characters = charset; + } + + public List getCharacters() { + return Collections.unmodifiableList(characters); + } + + public static Unicharset readFrom(BufferedReader in) throws IOException { + final int size = Integer.parseInt(in.readLine()); + final List charset = new ArrayList<>(size); + + String line; + Scanner scanner; + while ((line = in.readLine()) != null) { + if (LONG_LINE.matcher(line).matches()) { + scanner = new Scanner(line); + scanner.useDelimiter(DELIM); + + final String text = scanner.next(); + final CharacterProperties props = + CharacterProperties.forByteCode(scanner.nextByte()); + final CharacterDimensions dims = new CharacterDimensions( + scanner.nextInt() /* min bottom */, + scanner.nextInt() /* max bottom */, + scanner.nextInt() /* min top */, + scanner.nextInt() /* max top */, + scanner.nextInt() /* min width */, + scanner.nextInt() /* max width */, + scanner.nextInt() /* min bearing */, + scanner.nextInt() /* max bearing */, + scanner.nextInt() /* min advance */, + scanner.nextInt() /* max advance */); + final String script = scanner.next(); + final int otherCase = scanner.nextInt(); + final int direction = scanner.nextInt(); + final int mirror = scanner.nextInt(); + final String normed = scanner.nextLine().trim(); + charset.add(new Char(text, props, dims, script, otherCase, + direction, mirror, normed)); + + scanner.close(); + } else if (FIRST_LINE.matcher(line).matches()) { + charset.add(new Char("NULL", + CharacterProperties.forByteCode((byte) 0), " ", 0)); + } + } + + in.close(); + + return new Unicharset(charset); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/tools/training/package-info.java b/tools/src/main/java/de/vorb/tesseract/tools/training/package-info.java new file mode 100644 index 00000000..e83f6fcf --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/tools/training/package-info.java @@ -0,0 +1,7 @@ +/** + * Tesseract training. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.tools.training; + diff --git a/tools/src/main/java/de/vorb/tesseract/util/AlternativeChoice.java b/tools/src/main/java/de/vorb/tesseract/util/AlternativeChoice.java new file mode 100644 index 00000000..03494846 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/AlternativeChoice.java @@ -0,0 +1,19 @@ +package de.vorb.tesseract.util; + +public class AlternativeChoice { + private final String text; + private final float confidence; + + public AlternativeChoice(String text, float confidence) { + this.text = text; + this.confidence = confidence; + } + + public String getText() { + return text; + } + + public float getConfidence() { + return confidence; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Baseline.java b/tools/src/main/java/de/vorb/tesseract/util/Baseline.java new file mode 100644 index 00000000..f0f6ff1d --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Baseline.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.util; + +public class Baseline extends Straight { + public Baseline(int yOffset, float slope) { + super(yOffset, slope); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Block.java b/tools/src/main/java/de/vorb/tesseract/util/Block.java new file mode 100644 index 00000000..f4e5e265 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Block.java @@ -0,0 +1,37 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.BoxAdapter; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collections; +import java.util.List; + +public class Block { + @XmlJavaTypeAdapter(BoxAdapter.class) + @XmlAttribute(name = "bounding-box") + private final Box boundingBox; + + @XmlElement(name = "paragraph") + private final List paragraphs; + + public Block(Box boundingBox, List paragraphs) { + this.boundingBox = boundingBox; + this.paragraphs = paragraphs; + } + + public List getParagraphs() { + return Collections.unmodifiableList(paragraphs); + } + + public Box getBoundingBox() { + return boundingBox; + } + + @Override + public String toString() { + return String.format("Block(boundingBox = %s, paragraphs = [%d])", boundingBox, + paragraphs.size()); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Box.java b/tools/src/main/java/de/vorb/tesseract/util/Box.java new file mode 100644 index 00000000..0e8e4be2 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Box.java @@ -0,0 +1,119 @@ +package de.vorb.tesseract.util; + +import java.awt.Rectangle; + +public class Box { + private int x; + private int y; + private int width; + private int height; + + public Box(int x, int y, int width, int height) { + setX(x); + setY(y); + setWidth(width); + setHeight(height); + } + + public int getX() { + return x; + } + + public void setX(int x) { + if (x < 0) { + throw new IllegalArgumentException("negative coordinate x"); + } + + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + if (y < 0) { + throw new IllegalArgumentException("negative coordinate y"); + } + + this.y = y; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + if (width < 0) { + throw new IllegalArgumentException("negative width"); + } + + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + if (height < 0) { + throw new IllegalArgumentException("negative height"); + } + + this.height = height; + } + + public int getArea() { + return width * height; + } + + public Rectangle toRectangle() { + return new Rectangle(x, y, width, height); + } + + public boolean contains(Point point) { + final int px = point.getX(), py = point.getY(); + return !(px < x || px > x + width || py < y || py > y + height); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + height; + result = prime * result + width; + result = prime * result + x; + result = prime * result + y; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (!(obj instanceof Box)) { + return false; + } + Box other = (Box) obj; + if (height != other.height) { + return false; + } else if (width != other.width) { + return false; + } else if (x != other.x) { + return false; + } + return y == other.y; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Box(x = %d, y = %d, width = %d, height = %d)", x, y, width, height); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/FontAttributes.java b/tools/src/main/java/de/vorb/tesseract/util/FontAttributes.java new file mode 100644 index 00000000..386c5591 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/FontAttributes.java @@ -0,0 +1,217 @@ +package de.vorb.tesseract.util; + +import javax.xml.bind.annotation.XmlAttribute; + +/** + * Font attributes of a recognized word. + * + * @author Paul Vorbach + */ + +/** + * @author Paul Vorbach + * + */ +public class FontAttributes { + + /** + * FontAttributes builder. + * + * @author Paul Vorbach + */ + public static class Builder { + private boolean bold; + private boolean italic; + private boolean underlined; + private boolean monospace; + private boolean serif; + private boolean smallCaps; + private int size; + private int fontID; + + public Builder() { + } + + public Builder bold(boolean bold) { + this.bold = bold; + return this; + } + + public Builder italic(boolean italic) { + this.italic = italic; + return this; + } + + public Builder underlined(boolean underlined) { + this.underlined = underlined; + return this; + } + + public Builder monospace(boolean monospace) { + this.monospace = monospace; + return this; + } + + public Builder serif(boolean serif) { + this.serif = serif; + return this; + } + + public Builder smallCaps(boolean smallCaps) { + this.smallCaps = smallCaps; + return this; + } + + public Builder size(int size) { + this.size = size; + return this; + } + + public Builder fontID(int fontID) { + this.fontID = fontID; + return this; + } + + /** + * Finalize the FontAttributes object. + * + * @return FontAttributes object + */ + public FontAttributes build() { + return new FontAttributes(bold, italic, underlined, monospace, + serif, smallCaps, size, fontID); + } + } + + @XmlAttribute + private final boolean bold; + + @XmlAttribute + private final boolean italic; + + @XmlAttribute + private final boolean underlined; + + @XmlAttribute + private final boolean monospace; + + @XmlAttribute + private final boolean serif; + + @XmlAttribute + private final boolean smallCaps; + + @XmlAttribute + private final int size; + + @XmlAttribute + private final int fontID; + + /** + * Create a FontAttributes object. + * + * @param bold + * @param italic + * @param underlined + * @param monospace + * @param serif + * @param smallCaps + * @param size + * @param fontID + */ + protected FontAttributes(boolean bold, boolean italic, boolean underlined, + boolean monospace, boolean serif, boolean smallCaps, int size, + int fontID) { + this.bold = bold; + this.italic = italic; + this.underlined = underlined; + this.monospace = monospace; + this.serif = serif; + this.smallCaps = smallCaps; + this.size = size; + this.fontID = fontID; + } + + /** + * @return true if the word is bold. + */ + public boolean isBold() { + return bold; + } + + /** + * @return true if the word is italic. + */ + public boolean isItalic() { + return italic; + } + + /** + * @return true if the word is underlined. + */ + public boolean isUnderlined() { + return underlined; + } + + /** + * @return true if the word is set in a monospace font. + */ + public boolean isMonospace() { + return monospace; + } + + /** + * @return true if the word is set in a font with serifs. + */ + public boolean isSerif() { + return serif; + } + + /** + * @return true if the word is set in small-caps + */ + public boolean isSmallCaps() { + return smallCaps; + } + + /** + * @return size of the font in pt. + */ + public int getSize() { + return size; + } + + /** + * @return ID of the font as defined in the *.traineddata file. + */ + public int getFontID() { + return fontID; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format( + "size: %dpx, bold: %b, italic: %b, underlined: %b, monospace: %b, serif: %b, small caps: %b, font ID:" + + " %d", + size, bold, italic, underlined, monospace, serif, smallCaps, + fontID); + } + + /** + * Optionally return given String. + * + * @param cond + * condition + * @param str + * string + * @return str if condition holds, empty String otherwise. + */ + private static String opt(boolean cond, String str) { + return cond ? str : ""; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Line.java b/tools/src/main/java/de/vorb/tesseract/util/Line.java new file mode 100644 index 00000000..de2c77f4 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Line.java @@ -0,0 +1,50 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.BoxAdapter; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collections; +import java.util.List; + +public class Line { + @XmlJavaTypeAdapter(BoxAdapter.class) + @XmlAttribute(name = "bounding-box") + private final Box boundingBox; + + // @XmlAttribute(name = "baseline") + private final Baseline baseline; + + @XmlElement(name = "word") + private final List words; + + public Line(Box boundingBox, List words, Baseline baseline) { + this.boundingBox = boundingBox; + this.words = words; + this.baseline = baseline; + } + + public List getWords() { + return Collections.unmodifiableList(words); + } + + public Box getBoundingBox() { + return boundingBox; + } + + public Baseline getBaseline() { + return baseline; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Line(boundingBox = %s, words = [%d], baseline = %s)", + boundingBox, words.size(), baseline); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Page.java b/tools/src/main/java/de/vorb/tesseract/util/Page.java new file mode 100644 index 00000000..82feb7f8 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Page.java @@ -0,0 +1,243 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.PathAdapter; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.namespace.QName; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +public class Page implements Iterable { + @XmlJavaTypeAdapter(PathAdapter.class) + @XmlAttribute + private final Path file; + + @XmlAttribute + private final int width; + @XmlAttribute + private final int height; + @XmlAttribute + private final int resolution; + + @XmlElement(name = "block") + private final List blocks; + + public Page(Path file, int width, int height, int resolution, List blocks) { + this.file = file; + + if (width < 1) { + throw new IllegalArgumentException("width < 1"); + } + + this.width = width; + + if (height < 1) { + throw new IllegalArgumentException("height < 1"); + } + + this.height = height; + + if (resolution < 1) { + throw new IllegalArgumentException("resolution < 1"); + } + + this.resolution = resolution; + + this.blocks = blocks; + } + + public Path getFile() { + return file; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getResolution() { + return resolution; + } + + public List getBlocks() { + return Collections.unmodifiableList(blocks); + } + + public void writeTo(OutputStream w) + throws IOException, JAXBException { + final JAXBContext jc = JAXBContext.newInstance(Page.class); + final Marshaller marshaller = jc.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + final JAXBElement jaxbElement = new JAXBElement<>(new QName( + "page"), Page.class, this); + + marshaller.marshal(jaxbElement, w); + } + + public Iterator blockIterator() { + return blocks.iterator(); + } + + public Iterator paragraphIterator() { + return new ParagraphIterator(blockIterator()); + } + + public Iterator lineIterator() { + return new LineIterator(paragraphIterator()); + } + + public Iterator wordIterator() { + return new WordIterator(lineIterator()); + } + + public Iterator symbolIterator() { + return new SymbolIterator(wordIterator()); + } + + @Override + public Iterator iterator() { + return symbolIterator(); + } + + private static class ParagraphIterator implements Iterator { + final Iterator blockIt; + Iterator paraIt; + + ParagraphIterator(Iterator blockIt) { + this.blockIt = blockIt; + } + + @Override + public boolean hasNext() { + if (paraIt != null && paraIt.hasNext()) { + return true; + } else if (!blockIt.hasNext()) { + return false; + } else { + paraIt = blockIt.next().getParagraphs().iterator(); + + return paraIt.hasNext(); + } + } + + @Override + public Paragraph next() { + return paraIt.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + + } + } + + private static class LineIterator implements Iterator { + final Iterator paraIt; + Iterator lineIt; + + LineIterator(Iterator paraIt) { + this.paraIt = paraIt; + } + + @Override + public boolean hasNext() { + if (lineIt != null && lineIt.hasNext()) { + return true; + } else if (!paraIt.hasNext()) { + return false; + } else { + lineIt = paraIt.next().getLines().iterator(); + + return lineIt.hasNext(); + } + } + + @Override + public Line next() { + return lineIt.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static class WordIterator implements Iterator { + final Iterator lineIt; + Iterator wordIt; + + WordIterator(Iterator lineIt) { + this.lineIt = lineIt; + } + + @Override + public boolean hasNext() { + if (wordIt != null && wordIt.hasNext()) { + return true; + } else if (!lineIt.hasNext()) { + return false; + } else { + wordIt = lineIt.next().getWords().iterator(); + + return wordIt.hasNext(); + } + } + + @Override + public Word next() { + return wordIt.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static class SymbolIterator implements Iterator { + final Iterator wordIt; + Iterator symbolIt; + + SymbolIterator(Iterator wordIt) { + this.wordIt = wordIt; + } + + @Override + public boolean hasNext() { + if (symbolIt != null && symbolIt.hasNext()) { + return true; + } else if (!wordIt.hasNext()) { + return false; + } else { + symbolIt = wordIt.next().getSymbols().iterator(); + + return symbolIt.hasNext(); + } + } + + @Override + public Symbol next() { + return symbolIt.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Paragraph.java b/tools/src/main/java/de/vorb/tesseract/util/Paragraph.java new file mode 100644 index 00000000..685ef003 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Paragraph.java @@ -0,0 +1,37 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.BoxAdapter; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collections; +import java.util.List; + +public class Paragraph { + @XmlJavaTypeAdapter(BoxAdapter.class) + @XmlAttribute(name = "bounding-box") + private final Box boundingBox; + + @XmlElement(name = "line") + private final List lines; + + public Paragraph(Box boundingBox, List lines) { + this.boundingBox = boundingBox; + this.lines = lines; + } + + public List getLines() { + return Collections.unmodifiableList(lines); + } + + public Box getBoundingBox() { + return boundingBox; + } + + @Override + public String toString() { + return String.format("Paragraph(boundingBox = %s, lines = [%d])", boundingBox, + lines.size()); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Point.java b/tools/src/main/java/de/vorb/tesseract/util/Point.java new file mode 100644 index 00000000..c49e3409 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Point.java @@ -0,0 +1,65 @@ +package de.vorb.tesseract.util; + +public class Point { + private final int x, y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + public Point(java.awt.Point p) { + this.x = p.x; + this.y = p.y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (!(obj instanceof Point)) { + return false; + } + Point other = (Point) obj; + return x == other.x && y == other.y; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Point(x = " + x + ", y = " + y + ")"; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Straight.java b/tools/src/main/java/de/vorb/tesseract/util/Straight.java new file mode 100644 index 00000000..993ba05e --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Straight.java @@ -0,0 +1,29 @@ +package de.vorb.tesseract.util; + +public abstract class Straight { + private final float m; + private final int c; + + public Straight(int yOffset, float slope) { + this.c = yOffset; + this.m = slope; + } + + public int getYOffset() { + return c; + } + + public float getSlope() { + return m; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "f(x) = " + m + " * x + " + c; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Symbol.java b/tools/src/main/java/de/vorb/tesseract/util/Symbol.java new file mode 100644 index 00000000..b64aee06 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Symbol.java @@ -0,0 +1,104 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.BoxAdapter; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Collections; +import java.util.List; + +/** + * Recognized Symbol. Can either be a single character or a ligature or + * otherwise combined glyph. + * + * @author Paul Vorbach + */ +public class Symbol { + private String text; + private Box boundingBox; + private final float confidence; + private final List alternatives; + + /** + * Creates a new Symbol. + * + * @param text recognized text + * @param boundingBox bounding box + * @param confidence recognition confidence + */ + public Symbol(String text, Box boundingBox, float confidence) { + this.text = text; + this.boundingBox = boundingBox; + this.confidence = confidence; + this.alternatives = Collections.emptyList(); + } + + /** + * Creates a new Symbol. + * + * @param text recognized text + * @param boundingBox bounding box + * @param confidence recognition confidence + * @param alternatives alternative choices + */ + public Symbol(String text, Box boundingBox, float confidence, + List alternatives) { + this.text = text; + this.boundingBox = boundingBox; + this.confidence = confidence; + this.alternatives = alternatives; + } + + /** + * @return recognized text + */ + @XmlValue + public String getText() { + return text; + } + + /** + * Sets text. + * + * @param text + */ + public void setText(String text) { + this.text = text; + } + + /** + * @return recognition confidence + */ + @XmlAttribute + public float getConfidence() { + return confidence; + } + + /** + * @return bounding box + */ + @XmlAttribute(name = "bounding-box") + @XmlJavaTypeAdapter(BoxAdapter.class) + public Box getBoundingBox() { + return boundingBox; + } + + /** + * @return alternative choices + */ + public List getAlternatives() { + return alternatives; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Symbol(" + text + ", bounds = " + boundingBox + ", conf = " + + confidence + ")"; + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/TraineddataFiles.java b/tools/src/main/java/de/vorb/tesseract/util/TraineddataFiles.java new file mode 100644 index 00000000..bfaf0cee --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/TraineddataFiles.java @@ -0,0 +1,70 @@ +package de.vorb.tesseract.util; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedList; +import java.util.List; + +public final class TraineddataFiles { + private TraineddataFiles() { + } + + private static final DirectoryStream.Filter traineddataFilter = + f -> Files.isRegularFile(f) + && f.getFileName().toString().endsWith( + ".traineddata"); + + /** + * Lists all available traineddata files in the given directory. + * + * @return List of available traineddata files. + * @throws IOException if the directory does not exist or cannot be read + */ + public static List getAvailable(Path tessdataDir) + throws IOException { + final DirectoryStream dir = Files.newDirectoryStream(tessdataDir, + traineddataFilter); + + final LinkedList languages = new LinkedList<>(); + for (final Path languageFile : dir) { + final String language = languageFile.getFileName().toString().replaceFirst("\\.traineddata$", ""); + languages.add(language); + } + + return languages; + } + + /** + * Lists all available traineddata files in the directory + * {@code $TESSDATA_PREFIX/tessdata}. + * + * @return List of available traineddata files. + * @throws IOException if the directory does not exist or cannot be read + */ + public static List getAvailable() throws IOException { + return getAvailable(getTessdataDir()); + } + + public static Path getTessdataDir() { + final String tessdataPrefix = System.getenv("TESSDATA_PREFIX"); + Path tessdataDir; + if (tessdataPrefix != null) { + tessdataDir = Paths.get(tessdataPrefix).resolve("tessdata"); + + if (Files.isDirectory(tessdataDir) && Files.isReadable(tessdataDir)) { + return tessdataDir; + } + } + + tessdataDir = Paths.get("tessdata").toAbsolutePath(); + + if (Files.isDirectory(tessdataDir) && Files.isReadable(tessdataDir)) { + return tessdataDir; + } else { + return Paths.get(""); + } + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/Word.java b/tools/src/main/java/de/vorb/tesseract/util/Word.java new file mode 100644 index 00000000..da15382f --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/Word.java @@ -0,0 +1,78 @@ +package de.vorb.tesseract.util; + +import de.vorb.tesseract.util.xml.BaselineAdapter; +import de.vorb.tesseract.util.xml.BoxAdapter; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.List; + +public class Word { + @XmlElement(name = "symbol") + private final List symbols; + + @XmlJavaTypeAdapter(BoxAdapter.class) + @XmlAttribute(name = "bounding-box") + private final Box boundingBox; + + private final float conf; + + @XmlJavaTypeAdapter(BaselineAdapter.class) + @XmlAttribute + private final Baseline baseline; + + @XmlElement(name = "font-attributes") + private final FontAttributes fontAttrs; + + private boolean correct = true; + + public Word(List symbols, Box boundingBox, float conf, Baseline baseline, + FontAttributes fontAttrs) { + this.symbols = symbols; + this.boundingBox = boundingBox; + this.conf = conf; + this.baseline = baseline; + this.fontAttrs = fontAttrs; + } + + public List getSymbols() { + return symbols; + } + + public Box getBoundingBox() { + return boundingBox; + } + + @XmlAttribute(name = "confidence") + public float getConfidence() { + return conf; + } + + @XmlAttribute(name = "correct") + public boolean isCorrect() { + return correct; + } + + public void setCorrect(boolean correct) { + this.correct = correct; + } + + public Baseline getBaseline() { + return baseline; + } + + public FontAttributes getFontAttributes() { + return fontAttrs; + } + + public String getText() { + final StringBuilder text = new StringBuilder(); + + for (final Symbol s : symbols) { + text.append(s.getText()); + } + + return text.toString(); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/XLine.java b/tools/src/main/java/de/vorb/tesseract/util/XLine.java new file mode 100644 index 00000000..862df1ac --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/XLine.java @@ -0,0 +1,7 @@ +package de.vorb.tesseract.util; + +public class XLine extends Baseline { + public XLine(int yOffset, float slope) { + super(yOffset, slope); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/feat/Feature3D.java b/tools/src/main/java/de/vorb/tesseract/util/feat/Feature3D.java new file mode 100644 index 00000000..2a0ce90d --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/feat/Feature3D.java @@ -0,0 +1,63 @@ +package de.vorb.tesseract.util.feat; + +import org.bytedeco.javacpp.tesseract; + +import java.nio.ByteBuffer; + +public class Feature3D { + private final byte x, y, theta; // features + private final byte cpMisses; // cp misses + private final int outlineIndex; // index of the outline containing this feature + + public Feature3D(int x, int y, int theta) { + this.x = (byte) x; + this.y = (byte) y; + this.theta = (byte) theta; + cpMisses = 0; + outlineIndex = 0; + } + + public Feature3D(int x, int y, int theta, int cpMisses, int outlineIndex) { + this.x = (byte) x; + this.y = (byte) (0xFF - y); + this.theta = (byte) (0xFF - theta); + this.cpMisses = (byte) cpMisses; + this.outlineIndex = outlineIndex; + } + + public int getX() { + return x & 0xFF; + } + + public int getY() { + return y & 0xFF; + } + + public int getTheta() { + return theta & 0xFF; + } + + public int getCPMisses() { + return cpMisses; + } + + public int getOutlineIndex() { + return outlineIndex; + } + + public static Feature3D valueOf(tesseract.INT_FEATURE_STRUCT feat, int outlineIndex) { + final ByteBuffer buf = feat.asByteBuffer(); + return new Feature3D( + buf.get(0) & 0xFF, + buf.get(1) & 0xFF, + buf.get(2) & 0xFF, + buf.get(3) & 0xFF, + outlineIndex); + } + + @Override + public String toString() { + return String.format("Feature3D(x = %d, y = %d, theta = %d)", + getX(), getY(), getTheta()); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/feat/Feature4D.java b/tools/src/main/java/de/vorb/tesseract/util/feat/Feature4D.java new file mode 100644 index 00000000..c2f06e2f --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/feat/Feature4D.java @@ -0,0 +1,43 @@ +package de.vorb.tesseract.util.feat; + +public class Feature4D { + private final byte a; + private final byte b; // unsigned byte + private final byte c; + private final byte angle; // unsigned byte + private final int[] configs; + + public Feature4D(int a, int b, int c, int angle, int[] configs) { + this.a = (byte) a; + this.b = (byte) b; + this.c = (byte) c; + this.angle = (byte) angle; + this.configs = configs; + } + + public int getA() { + return a; + } + + public int getB() { + return b & 0xFF; + } + + public int getC() { + return c; + } + + public int getAngle() { + return angle & 0xFF; + } + + public long getConfig(int i) { + return configs[i] & 0xFFFF_FFFFL; + } + + @Override + public String toString() { + return String.format("Feature4D(a=%d, b=%d, c=%d, angle=%d)", getA(), + getB(), getC(), getAngle()); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/package-info.java b/tools/src/main/java/de/vorb/tesseract/util/package-info.java new file mode 100644 index 00000000..d7c06fb6 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Utilities for the Tesseract tools. + * + * @author Paul Vorbach + */ +package de.vorb.tesseract.util; + diff --git a/tools/src/main/java/de/vorb/tesseract/util/xml/BaselineAdapter.java b/tools/src/main/java/de/vorb/tesseract/util/xml/BaselineAdapter.java new file mode 100644 index 00000000..0b6102c7 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/xml/BaselineAdapter.java @@ -0,0 +1,21 @@ +package de.vorb.tesseract.util.xml; + +import de.vorb.tesseract.util.Baseline; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class BaselineAdapter extends XmlAdapter { + + @Override + public String marshal(Baseline baseline) throws Exception { + return baseline.getYOffset() + " " + baseline.getSlope(); + } + + @Override + public Baseline unmarshal(String baseline) throws Exception { + final String[] coefficients = baseline.trim().split("\\s+"); + return new Baseline(Integer.parseInt(coefficients[0]), + Float.parseFloat(coefficients[1])); + } + +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/xml/BoxAdapter.java b/tools/src/main/java/de/vorb/tesseract/util/xml/BoxAdapter.java new file mode 100644 index 00000000..43865692 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/xml/BoxAdapter.java @@ -0,0 +1,22 @@ +package de.vorb.tesseract.util.xml; + +import de.vorb.tesseract.util.Box; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class BoxAdapter extends XmlAdapter { + @Override + public String marshal(Box box) throws Exception { + return box.getX() + " " + box.getY() + " " + box.getWidth() + " " + + box.getHeight(); + } + + @Override + public Box unmarshal(String box) throws Exception { + final String[] coordinates = box.trim().split(" "); + return new Box(Integer.parseInt(coordinates[0]), + Integer.parseInt(coordinates[1]), + Integer.parseInt(coordinates[2]), + Integer.parseInt(coordinates[3])); + } +} diff --git a/tools/src/main/java/de/vorb/tesseract/util/xml/PathAdapter.java b/tools/src/main/java/de/vorb/tesseract/util/xml/PathAdapter.java new file mode 100644 index 00000000..56637098 --- /dev/null +++ b/tools/src/main/java/de/vorb/tesseract/util/xml/PathAdapter.java @@ -0,0 +1,17 @@ +package de.vorb.tesseract.util.xml; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class PathAdapter extends XmlAdapter { + @Override + public String marshal(Path path) throws Exception { + return path.toAbsolutePath().toString(); + } + + @Override + public Path unmarshal(String path) throws Exception { + return Paths.get(path.trim()); + } +} diff --git a/tools/src/main/java/de/vorb/util/FileNames.java b/tools/src/main/java/de/vorb/util/FileNames.java new file mode 100644 index 00000000..fe42f373 --- /dev/null +++ b/tools/src/main/java/de/vorb/util/FileNames.java @@ -0,0 +1,20 @@ +package de.vorb.util; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class FileNames { + + private FileNames() {} + + public static Path replaceExtension(Path file, String newExtension) { + final String filename = file.getFileName().toString(); + final int lastDot = filename.lastIndexOf('.'); + if (file.getParent() == null) { + return Paths.get(filename.substring(0, lastDot + 1) + newExtension); + } else { + return file.getParent().resolve( + filename.substring(0, lastDot + 1) + newExtension); + } + } +} diff --git a/tools/src/main/java/de/vorb/util/StringDistance.java b/tools/src/main/java/de/vorb/util/StringDistance.java new file mode 100644 index 00000000..814fbf2b --- /dev/null +++ b/tools/src/main/java/de/vorb/util/StringDistance.java @@ -0,0 +1,54 @@ +package de.vorb.util; + +/** + * Distance metrics for strings. + * + * @author Paul Vorbach + */ +public class StringDistance { + private static StringDistance instance = null; + + private StringDistance() { + } + + /** + * @return singleton instance of this class. + */ + public static StringDistance getInstance() { + if (instance == null) + instance = new StringDistance(); + + return instance; + } + + /** + * Calculates the Levenshtein distance of two Strings. + *

+ * This implementation has a complexity of O(nm), where n is the + * length of a and m b. + * + * @param a first string + * @param b second string + * @return Levenshtein distance between a and b + */ + public int levenshtein(String a, String b) { + int[] costs = new int[b.length() + 1]; + + for (int j = 0; j < costs.length; j++) + costs[j] = j; + + for (int i = 1; i <= a.length(); i++) { + costs[0] = i; + int nw = i - 1; + + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), + a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + + return costs[b.length()]; + } +} diff --git a/tools/src/main/java/de/vorb/util/package-info.java b/tools/src/main/java/de/vorb/util/package-info.java new file mode 100644 index 00000000..74b7f08f --- /dev/null +++ b/tools/src/main/java/de/vorb/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Common utilities. + * + * @author Paul Vorbach + */ +package de.vorb.util; + diff --git a/src/main/resources/arrow-left.png b/tools/src/main/resources/arrow-left.png similarity index 100% rename from src/main/resources/arrow-left.png rename to tools/src/main/resources/arrow-left.png diff --git a/src/main/resources/arrow-right.png b/tools/src/main/resources/arrow-right.png similarity index 100% rename from src/main/resources/arrow-right.png rename to tools/src/main/resources/arrow-right.png diff --git a/tools/src/test/java/de/vorb/tesseract/tools/training/CharacterPropertiesTest.java b/tools/src/test/java/de/vorb/tesseract/tools/training/CharacterPropertiesTest.java new file mode 100644 index 00000000..d0b5ffbb --- /dev/null +++ b/tools/src/test/java/de/vorb/tesseract/tools/training/CharacterPropertiesTest.java @@ -0,0 +1,84 @@ +package de.vorb.tesseract.tools.training; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class CharacterPropertiesTest { + final CharacterProperties semicolon = new CharacterProperties(false, false, + false, false, true); + final CharacterProperties b = new CharacterProperties(true, false, false, + true, false); + final CharacterProperties W = new CharacterProperties(true, false, true, + false, false); + final CharacterProperties digit7 = new CharacterProperties(false, true, + false, false, false); + final CharacterProperties equalSign = new CharacterProperties(false, false, + false, false, false); + final CharacterProperties sharpS = new CharacterProperties(true, false, + false, true, false); + final CharacterProperties umlautA = new CharacterProperties(true, false, + true, false, false); + + @Test + public void testForByteCode() { + assertEquals(CharacterProperties.forByteCode((byte) 16), semicolon); + assertEquals(CharacterProperties.forByteCode((byte) 3), b); + assertEquals(CharacterProperties.forByteCode((byte) 5), W); + assertEquals(CharacterProperties.forByteCode((byte) 8), digit7); + assertEquals(CharacterProperties.forByteCode((byte) 0), equalSign); + } + + @Test + public void testFromHexString() { + assertEquals(CharacterProperties.forHexString("10"), semicolon); + assertEquals(CharacterProperties.forHexString("3"), b); + assertEquals(CharacterProperties.forHexString("5"), W); + assertEquals(CharacterProperties.forHexString("8"), digit7); + assertEquals(CharacterProperties.forHexString("0"), equalSign); + } + + @Test + public void testForCharacter() { + assertEquals(CharacterProperties.forCharacter(';'), semicolon); + assertEquals(CharacterProperties.forCharacter('b'), b); + assertEquals(CharacterProperties.forCharacter('W'), W); + assertEquals(CharacterProperties.forCharacter('7'), digit7); + assertEquals(CharacterProperties.forCharacter('='), equalSign); + + // unicode characters + assertEquals(CharacterProperties.forCharacter('ß'), sharpS); + assertEquals(CharacterProperties.forCharacter('Ä'), umlautA); + } + + @Test + public void testToByteCode() { + assertEquals(semicolon.toByteCode(), (byte) 16); + assertEquals(b.toByteCode(), (byte) 3); + assertEquals(W.toByteCode(), (byte) 5); + assertEquals(digit7.toByteCode(), (byte) 8); + assertEquals(equalSign.toByteCode(), (byte) 0); + } + + @Test + public void testToHexString() { + assertEquals(semicolon.toHexString(), "10"); + assertEquals(b.toHexString(), "3"); + assertEquals(W.toHexString(), "5"); + assertEquals(digit7.toHexString(), "8"); + assertEquals(equalSign.toHexString(), "0"); + } + + @Test + public void testEquals() { + assertEquals(b.equals(b), true); + assertEquals(b.equals(W), false); + } + + @Test + public void testHashCode() { + assertEquals(b.hashCode(), b.hashCode()); + assertNotEquals(b.hashCode(), W.hashCode()); + } +} diff --git a/tools/src/test/java/de/vorb/tesseract/tools/training/EmptyInputBufferTest.java b/tools/src/test/java/de/vorb/tesseract/tools/training/EmptyInputBufferTest.java new file mode 100644 index 00000000..24d718ad --- /dev/null +++ b/tools/src/test/java/de/vorb/tesseract/tools/training/EmptyInputBufferTest.java @@ -0,0 +1,54 @@ +package de.vorb.tesseract.tools.training; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class EmptyInputBufferTest { + private InputBuffer empty; + + @Before + public void setUp() throws Exception { + empty = InputBuffer.allocate(new ByteArrayInputStream(new byte[0]), + 4096); + } + + @Test + public void testReadByte() { + try { + Assert.assertFalse("could read from empty buffer", + empty.readByte()); + } catch (IOException e) { + } + } + + @Test + public void testReadShort() { + try { + Assert.assertFalse("could read from empty buffer", + empty.readShort()); + } catch (IOException e) { + } + } + + @Test + public void testReadInt() { + try { + Assert.assertFalse("could read from empty buffer", + empty.readInt()); + } catch (IOException e) { + } + } + + @Test + public void testReadLong() { + try { + Assert.assertFalse("could read from empty buffer", + empty.readLong()); + } catch (IOException e) { + } + } +} diff --git a/tools/src/test/java/de/vorb/tesseract/tools/training/SmallInputBufferTest.java b/tools/src/test/java/de/vorb/tesseract/tools/training/SmallInputBufferTest.java new file mode 100644 index 00000000..cc932bb0 --- /dev/null +++ b/tools/src/test/java/de/vorb/tesseract/tools/training/SmallInputBufferTest.java @@ -0,0 +1,63 @@ +package de.vorb.tesseract.tools.training; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class SmallInputBufferTest { + private static final byte[] bytes = new byte[256]; + + @Test + public void testReadByte() throws IOException { + final InputBuffer buf = InputBuffer.allocate(new ByteArrayInputStream( + bytes), 4096); + + int i = 0; + while (buf.readByte()) { + i++; + } + + Assert.assertEquals("preliminary end of stream", bytes.length, i); + } + + @Test + public void testReadShort() throws IOException { + final InputBuffer buf = InputBuffer.allocate(new ByteArrayInputStream( + bytes), 4096); + + int i = 0; + while (buf.readShort()) { + i++; + } + + Assert.assertEquals("preliminary end of stream", bytes.length / 2, i); + } + + @Test + public void testReadInt() throws IOException { + final InputBuffer buf = InputBuffer.allocate(new ByteArrayInputStream( + bytes), 4096); + + int i = 0; + while (buf.readInt()) { + i++; + } + + Assert.assertEquals("preliminary end of stream", bytes.length / 4, i); + } + + @Test + public void testReadLong() throws IOException { + final InputBuffer buf = InputBuffer.allocate(new ByteArrayInputStream( + bytes), 4096); + + int i = 0; + while (buf.readLong()) { + i++; + } + + Assert.assertEquals("preliminary end of stream", bytes.length / 8, i); + } +}