clone

a photo toolkit for your phone that never uploads: fuji film looks for raw files, social crops, error level analysis

gud.pics: mobile-first photo toolkit with perspective tool

Dami committed e716e07
commit
e716e076f2caad416e3ccd7f65e180da3069d282
author
Dami <dami.etoile@gmail.com>
36 files changed, +15286 -0
A.gitignore4+
ALICENSES/AGPL-3.0.txt661+++
ALICENSES/EMSCRIPTEN.txt102+
ALICENSES/GPL-3.0.txt674+++
ANOTICE.md31+
AREADME.md61+
A_headers16+
Aapp.mjs157+
Aassets/core.mjs2+
Aassets/core.wasmbin
Aassets/icon-192.pngbin
Aassets/icon-512.pngbin
Aassets/icon.svg1+
Ageometry.mjs50+
Ahome.mjs15+
Aindex.html62+
Amanifest.webmanifest13+
Anative/compat.h41+
Anative/core.c76+
Anative/extracted.inc1460+++++++
Anative/lsd.inc2283+++++++++++
Anative/upstream/REVISION1+
Anative/upstream/ashift.c6223++++++++++++++++++++++++++++++
Anative/upstream/ashift_lsd.c2283+++++++++++
Anative/upstream/ashift_nmsimplex.c420++
Aperspective.html97+
Ascripts/build.sh10+
Ascripts/extract.py48+
Ascripts/icons.py16+
Ascripts/package.py17+
Ascripts/serve.py23+
Ascripts/test-core.mjs51+
Astyle.css301+
Asw.js17+
Aworker.mjs64+
Awrangler.jsonc6+
A .gitignore +4 -0
A LICENSES/AGPL-3.0.txt +661 -0
@@ -0,0 +1,661 @@
1+ GNU AFFERO GENERAL PUBLIC LICENSE2+ Version 3, 19 November 20073+4+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>5+ Everyone is permitted to copy and distribute verbatim copies6+ of this license document, but changing it is not allowed.7+8+ Preamble9+10+ The GNU Affero General Public License is a free, copyleft license for11+software and other kinds of works, specifically designed to ensure12+cooperation with the community in the case of network server software.13+14+ The licenses for most software and other practical works are designed15+to take away your freedom to share and change the works. By contrast,16+our General Public Licenses are intended to guarantee your freedom to17+share and change all versions of a program--to make sure it remains free18+software for all its users.19+20+ When we speak of free software, we are referring to freedom, not21+price. Our General Public Licenses are designed to make sure that you22+have the freedom to distribute copies of free software (and charge for23+them if you wish), that you receive source code or can get it if you24+want it, that you can change the software or use pieces of it in new25+free programs, and that you know you can do these things.26+27+ Developers that use our General Public Licenses protect your rights28+with two steps: (1) assert copyright on the software, and (2) offer29+you this License which gives you legal permission to copy, distribute30+and/or modify the software.31+32+ A secondary benefit of defending all users' freedom is that33+improvements made in alternate versions of the program, if they34+receive widespread use, become available for other developers to35+incorporate. Many developers of free software are heartened and36+encouraged by the resulting cooperation. However, in the case of37+software used on network servers, this result may fail to come about.38+The GNU General Public License permits making a modified version and39+letting the public access it on a server without ever releasing its40+source code to the public.41+42+ The GNU Affero General Public License is designed specifically to43+ensure that, in such cases, the modified source code becomes available44+to the community. It requires the operator of a network server to45+provide the source code of the modified version running there to the46+users of that server. Therefore, public use of a modified version, on47+a publicly accessible server, gives the public access to the source48+code of the modified version.49+50+ An older license, called the Affero General Public License and51+published by Affero, was designed to accomplish similar goals. This is52+a different license, not a version of the Affero GPL, but Affero has53+released a new version of the Affero GPL which permits relicensing under54+this license.55+56+ The precise terms and conditions for copying, distribution and57+modification follow.58+59+ TERMS AND CONDITIONS60+61+ 0. Definitions.62+63+ "This License" refers to version 3 of the GNU Affero General Public License.64+65+ "Copyright" also means copyright-like laws that apply to other kinds of66+works, such as semiconductor masks.67+68+ "The Program" refers to any copyrightable work licensed under this69+License. Each licensee is addressed as "you". "Licensees" and70+"recipients" may be individuals or organizations.71+72+ To "modify" a work means to copy from or adapt all or part of the work73+in a fashion requiring copyright permission, other than the making of an74+exact copy. The resulting work is called a "modified version" of the75+earlier work or a work "based on" the earlier work.76+77+ A "covered work" means either the unmodified Program or a work based78+on the Program.79+80+ To "propagate" a work means to do anything with it that, without81+permission, would make you directly or secondarily liable for82+infringement under applicable copyright law, except executing it on a83+computer or modifying a private copy. Propagation includes copying,84+distribution (with or without modification), making available to the85+public, and in some countries other activities as well.86+87+ To "convey" a work means any kind of propagation that enables other88+parties to make or receive copies. Mere interaction with a user through89+a computer network, with no transfer of a copy, is not conveying.90+91+ An interactive user interface displays "Appropriate Legal Notices"92+to the extent that it includes a convenient and prominently visible93+feature that (1) displays an appropriate copyright notice, and (2)94+tells the user that there is no warranty for the work (except to the95+extent that warranties are provided), that licensees may convey the96+work under this License, and how to view a copy of this License. If97+the interface presents a list of user commands or options, such as a98+menu, a prominent item in the list meets this criterion.99+100+ 1. Source Code.101+102+ The "source code" for a work means the preferred form of the work103+for making modifications to it. "Object code" means any non-source104+form of a work.105+106+ A "Standard Interface" means an interface that either is an official107+standard defined by a recognized standards body, or, in the case of108+interfaces specified for a particular programming language, one that109+is widely used among developers working in that language.110+111+ The "System Libraries" of an executable work include anything, other112+than the work as a whole, that (a) is included in the normal form of113+packaging a Major Component, but which is not part of that Major114+Component, and (b) serves only to enable use of the work with that115+Major Component, or to implement a Standard Interface for which an116+implementation is available to the public in source code form. A117+"Major Component", in this context, means a major essential component118+(kernel, window system, and so on) of the specific operating system119+(if any) on which the executable work runs, or a compiler used to120+produce the work, or an object code interpreter used to run it.121+122+ The "Corresponding Source" for a work in object code form means all123+the source code needed to generate, install, and (for an executable124+work) run the object code and to modify the work, including scripts to125+control those activities. However, it does not include the work's126+System Libraries, or general-purpose tools or generally available free127+programs which are used unmodified in performing those activities but128+which are not part of the work. For example, Corresponding Source129+includes interface definition files associated with source files for130+the work, and the source code for shared libraries and dynamically131+linked subprograms that the work is specifically designed to require,132+such as by intimate data communication or control flow between those133+subprograms and other parts of the work.134+135+ The Corresponding Source need not include anything that users136+can regenerate automatically from other parts of the Corresponding137+Source.138+139+ The Corresponding Source for a work in source code form is that140+same work.141+142+ 2. Basic Permissions.143+144+ All rights granted under this License are granted for the term of145+copyright on the Program, and are irrevocable provided the stated146+conditions are met. This License explicitly affirms your unlimited147+permission to run the unmodified Program. The output from running a148+covered work is covered by this License only if the output, given its149+content, constitutes a covered work. This License acknowledges your150+rights of fair use or other equivalent, as provided by copyright law.151+152+ You may make, run and propagate covered works that you do not153+convey, without conditions so long as your license otherwise remains154+in force. You may convey covered works to others for the sole purpose155+of having them make modifications exclusively for you, or provide you156+with facilities for running those works, provided that you comply with157+the terms of this License in conveying all material for which you do158+not control copyright. Those thus making or running the covered works159+for you must do so exclusively on your behalf, under your direction160+and control, on terms that prohibit them from making any copies of161+your copyrighted material outside their relationship with you.162+163+ Conveying under any other circumstances is permitted solely under164+the conditions stated below. Sublicensing is not allowed; section 10165+makes it unnecessary.166+167+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.168+169+ No covered work shall be deemed part of an effective technological170+measure under any applicable law fulfilling obligations under article171+11 of the WIPO copyright treaty adopted on 20 December 1996, or172+similar laws prohibiting or restricting circumvention of such173+measures.174+175+ When you convey a covered work, you waive any legal power to forbid176+circumvention of technological measures to the extent such circumvention177+is effected by exercising rights under this License with respect to178+the covered work, and you disclaim any intention to limit operation or179+modification of the work as a means of enforcing, against the work's180+users, your or third parties' legal rights to forbid circumvention of181+technological measures.182+183+ 4. Conveying Verbatim Copies.184+185+ You may convey verbatim copies of the Program's source code as you186+receive it, in any medium, provided that you conspicuously and187+appropriately publish on each copy an appropriate copyright notice;188+keep intact all notices stating that this License and any189+non-permissive terms added in accord with section 7 apply to the code;190+keep intact all notices of the absence of any warranty; and give all191+recipients a copy of this License along with the Program.192+193+ You may charge any price or no price for each copy that you convey,194+and you may offer support or warranty protection for a fee.195+196+ 5. Conveying Modified Source Versions.197+198+ You may convey a work based on the Program, or the modifications to199+produce it from the Program, in the form of source code under the200+terms of section 4, provided that you also meet all of these conditions:201+202+ a) The work must carry prominent notices stating that you modified203+ it, and giving a relevant date.204+205+ b) The work must carry prominent notices stating that it is206+ released under this License and any conditions added under section207+ 7. This requirement modifies the requirement in section 4 to208+ "keep intact all notices".209+210+ c) You must license the entire work, as a whole, under this211+ License to anyone who comes into possession of a copy. This212+ License will therefore apply, along with any applicable section 7213+ additional terms, to the whole of the work, and all its parts,214+ regardless of how they are packaged. This License gives no215+ permission to license the work in any other way, but it does not216+ invalidate such permission if you have separately received it.217+218+ d) If the work has interactive user interfaces, each must display219+ Appropriate Legal Notices; however, if the Program has interactive220+ interfaces that do not display Appropriate Legal Notices, your221+ work need not make them do so.222+223+ A compilation of a covered work with other separate and independent224+works, which are not by their nature extensions of the covered work,225+and which are not combined with it such as to form a larger program,226+in or on a volume of a storage or distribution medium, is called an227+"aggregate" if the compilation and its resulting copyright are not228+used to limit the access or legal rights of the compilation's users229+beyond what the individual works permit. Inclusion of a covered work230+in an aggregate does not cause this License to apply to the other231+parts of the aggregate.232+233+ 6. Conveying Non-Source Forms.234+235+ You may convey a covered work in object code form under the terms236+of sections 4 and 5, provided that you also convey the237+machine-readable Corresponding Source under the terms of this License,238+in one of these ways:239+240+ a) Convey the object code in, or embodied in, a physical product241+ (including a physical distribution medium), accompanied by the242+ Corresponding Source fixed on a durable physical medium243+ customarily used for software interchange.244+245+ b) Convey the object code in, or embodied in, a physical product246+ (including a physical distribution medium), accompanied by a247+ written offer, valid for at least three years and valid for as248+ long as you offer spare parts or customer support for that product249+ model, to give anyone who possesses the object code either (1) a250+ copy of the Corresponding Source for all the software in the251+ product that is covered by this License, on a durable physical252+ medium customarily used for software interchange, for a price no253+ more than your reasonable cost of physically performing this254+ conveying of source, or (2) access to copy the255+ Corresponding Source from a network server at no charge.256+257+ c) Convey individual copies of the object code with a copy of the258+ written offer to provide the Corresponding Source. This259+ alternative is allowed only occasionally and noncommercially, and260+ only if you received the object code with such an offer, in accord261+ with subsection 6b.262+263+ d) Convey the object code by offering access from a designated264+ place (gratis or for a charge), and offer equivalent access to the265+ Corresponding Source in the same way through the same place at no266+ further charge. You need not require recipients to copy the267+ Corresponding Source along with the object code. If the place to268+ copy the object code is a network server, the Corresponding Source269+ may be on a different server (operated by you or a third party)270+ that supports equivalent copying facilities, provided you maintain271+ clear directions next to the object code saying where to find the272+ Corresponding Source. Regardless of what server hosts the273+ Corresponding Source, you remain obligated to ensure that it is274+ available for as long as needed to satisfy these requirements.275+276+ e) Convey the object code using peer-to-peer transmission, provided277+ you inform other peers where the object code and Corresponding278+ Source of the work are being offered to the general public at no279+ charge under subsection 6d.280+281+ A separable portion of the object code, whose source code is excluded282+from the Corresponding Source as a System Library, need not be283+included in conveying the object code work.284+285+ A "User Product" is either (1) a "consumer product", which means any286+tangible personal property which is normally used for personal, family,287+or household purposes, or (2) anything designed or sold for incorporation288+into a dwelling. In determining whether a product is a consumer product,289+doubtful cases shall be resolved in favor of coverage. For a particular290+product received by a particular user, "normally used" refers to a291+typical or common use of that class of product, regardless of the status292+of the particular user or of the way in which the particular user293+actually uses, or expects or is expected to use, the product. A product294+is a consumer product regardless of whether the product has substantial295+commercial, industrial or non-consumer uses, unless such uses represent296+the only significant mode of use of the product.297+298+ "Installation Information" for a User Product means any methods,299+procedures, authorization keys, or other information required to install300+and execute modified versions of a covered work in that User Product from301+a modified version of its Corresponding Source. The information must302+suffice to ensure that the continued functioning of the modified object303+code is in no case prevented or interfered with solely because304+modification has been made.305+306+ If you convey an object code work under this section in, or with, or307+specifically for use in, a User Product, and the conveying occurs as308+part of a transaction in which the right of possession and use of the309+User Product is transferred to the recipient in perpetuity or for a310+fixed term (regardless of how the transaction is characterized), the311+Corresponding Source conveyed under this section must be accompanied312+by the Installation Information. But this requirement does not apply313+if neither you nor any third party retains the ability to install314+modified object code on the User Product (for example, the work has315+been installed in ROM).316+317+ The requirement to provide Installation Information does not include a318+requirement to continue to provide support service, warranty, or updates319+for a work that has been modified or installed by the recipient, or for320+the User Product in which it has been modified or installed. Access to a321+network may be denied when the modification itself materially and322+adversely affects the operation of the network or violates the rules and323+protocols for communication across the network.324+325+ Corresponding Source conveyed, and Installation Information provided,326+in accord with this section must be in a format that is publicly327+documented (and with an implementation available to the public in328+source code form), and must require no special password or key for329+unpacking, reading or copying.330+331+ 7. Additional Terms.332+333+ "Additional permissions" are terms that supplement the terms of this334+License by making exceptions from one or more of its conditions.335+Additional permissions that are applicable to the entire Program shall336+be treated as though they were included in this License, to the extent337+that they are valid under applicable law. If additional permissions338+apply only to part of the Program, that part may be used separately339+under those permissions, but the entire Program remains governed by340+this License without regard to the additional permissions.341+342+ When you convey a copy of a covered work, you may at your option343+remove any additional permissions from that copy, or from any part of344+it. (Additional permissions may be written to require their own345+removal in certain cases when you modify the work.) You may place346+additional permissions on material, added by you to a covered work,347+for which you have or can give appropriate copyright permission.348+349+ Notwithstanding any other provision of this License, for material you350+add to a covered work, you may (if authorized by the copyright holders of351+that material) supplement the terms of this License with terms:352+353+ a) Disclaiming warranty or limiting liability differently from the354+ terms of sections 15 and 16 of this License; or355+356+ b) Requiring preservation of specified reasonable legal notices or357+ author attributions in that material or in the Appropriate Legal358+ Notices displayed by works containing it; or359+360+ c) Prohibiting misrepresentation of the origin of that material, or361+ requiring that modified versions of such material be marked in362+ reasonable ways as different from the original version; or363+364+ d) Limiting the use for publicity purposes of names of licensors or365+ authors of the material; or366+367+ e) Declining to grant rights under trademark law for use of some368+ trade names, trademarks, or service marks; or369+370+ f) Requiring indemnification of licensors and authors of that371+ material by anyone who conveys the material (or modified versions of372+ it) with contractual assumptions of liability to the recipient, for373+ any liability that these contractual assumptions directly impose on374+ those licensors and authors.375+376+ All other non-permissive additional terms are considered "further377+restrictions" within the meaning of section 10. If the Program as you378+received it, or any part of it, contains a notice stating that it is379+governed by this License along with a term that is a further380+restriction, you may remove that term. If a license document contains381+a further restriction but permits relicensing or conveying under this382+License, you may add to a covered work material governed by the terms383+of that license document, provided that the further restriction does384+not survive such relicensing or conveying.385+386+ If you add terms to a covered work in accord with this section, you387+must place, in the relevant source files, a statement of the388+additional terms that apply to those files, or a notice indicating389+where to find the applicable terms.390+391+ Additional terms, permissive or non-permissive, may be stated in the392+form of a separately written license, or stated as exceptions;393+the above requirements apply either way.394+395+ 8. Termination.396+397+ You may not propagate or modify a covered work except as expressly398+provided under this License. Any attempt otherwise to propagate or399+modify it is void, and will automatically terminate your rights under400+this License (including any patent licenses granted under the third401+paragraph of section 11).402+403+ However, if you cease all violation of this License, then your404+license from a particular copyright holder is reinstated (a)405+provisionally, unless and until the copyright holder explicitly and406+finally terminates your license, and (b) permanently, if the copyright407+holder fails to notify you of the violation by some reasonable means408+prior to 60 days after the cessation.409+410+ Moreover, your license from a particular copyright holder is411+reinstated permanently if the copyright holder notifies you of the412+violation by some reasonable means, this is the first time you have413+received notice of violation of this License (for any work) from that414+copyright holder, and you cure the violation prior to 30 days after415+your receipt of the notice.416+417+ Termination of your rights under this section does not terminate the418+licenses of parties who have received copies or rights from you under419+this License. If your rights have been terminated and not permanently420+reinstated, you do not qualify to receive new licenses for the same421+material under section 10.422+423+ 9. Acceptance Not Required for Having Copies.424+425+ You are not required to accept this License in order to receive or426+run a copy of the Program. Ancillary propagation of a covered work427+occurring solely as a consequence of using peer-to-peer transmission428+to receive a copy likewise does not require acceptance. However,429+nothing other than this License grants you permission to propagate or430+modify any covered work. These actions infringe copyright if you do431+not accept this License. Therefore, by modifying or propagating a432+covered work, you indicate your acceptance of this License to do so.433+434+ 10. Automatic Licensing of Downstream Recipients.435+436+ Each time you convey a covered work, the recipient automatically437+receives a license from the original licensors, to run, modify and438+propagate that work, subject to this License. You are not responsible439+for enforcing compliance by third parties with this License.440+441+ An "entity transaction" is a transaction transferring control of an442+organization, or substantially all assets of one, or subdividing an443+organization, or merging organizations. If propagation of a covered444+work results from an entity transaction, each party to that445+transaction who receives a copy of the work also receives whatever446+licenses to the work the party's predecessor in interest had or could447+give under the previous paragraph, plus a right to possession of the448+Corresponding Source of the work from the predecessor in interest, if449+the predecessor has it or can get it with reasonable efforts.450+451+ You may not impose any further restrictions on the exercise of the452+rights granted or affirmed under this License. For example, you may453+not impose a license fee, royalty, or other charge for exercise of454+rights granted under this License, and you may not initiate litigation455+(including a cross-claim or counterclaim in a lawsuit) alleging that456+any patent claim is infringed by making, using, selling, offering for457+sale, or importing the Program or any portion of it.458+459+ 11. Patents.460+461+ A "contributor" is a copyright holder who authorizes use under this462+License of the Program or a work on which the Program is based. The463+work thus licensed is called the contributor's "contributor version".464+465+ A contributor's "essential patent claims" are all patent claims466+owned or controlled by the contributor, whether already acquired or467+hereafter acquired, that would be infringed by some manner, permitted468+by this License, of making, using, or selling its contributor version,469+but do not include claims that would be infringed only as a470+consequence of further modification of the contributor version. For471+purposes of this definition, "control" includes the right to grant472+patent sublicenses in a manner consistent with the requirements of473+this License.474+475+ Each contributor grants you a non-exclusive, worldwide, royalty-free476+patent license under the contributor's essential patent claims, to477+make, use, sell, offer for sale, import and otherwise run, modify and478+propagate the contents of its contributor version.479+480+ In the following three paragraphs, a "patent license" is any express481+agreement or commitment, however denominated, not to enforce a patent482+(such as an express permission to practice a patent or covenant not to483+sue for patent infringement). To "grant" such a patent license to a484+party means to make such an agreement or commitment not to enforce a485+patent against the party.486+487+ If you convey a covered work, knowingly relying on a patent license,488+and the Corresponding Source of the work is not available for anyone489+to copy, free of charge and under the terms of this License, through a490+publicly available network server or other readily accessible means,491+then you must either (1) cause the Corresponding Source to be so492+available, or (2) arrange to deprive yourself of the benefit of the493+patent license for this particular work, or (3) arrange, in a manner494+consistent with the requirements of this License, to extend the patent495+license to downstream recipients. "Knowingly relying" means you have496+actual knowledge that, but for the patent license, your conveying the497+covered work in a country, or your recipient's use of the covered work498+in a country, would infringe one or more identifiable patents in that499+country that you have reason to believe are valid.500+501+ If, pursuant to or in connection with a single transaction or502+arrangement, you convey, or propagate by procuring conveyance of, a503+covered work, and grant a patent license to some of the parties504+receiving the covered work authorizing them to use, propagate, modify505+or convey a specific copy of the covered work, then the patent license506+you grant is automatically extended to all recipients of the covered507+work and works based on it.508+509+ A patent license is "discriminatory" if it does not include within510+the scope of its coverage, prohibits the exercise of, or is511+conditioned on the non-exercise of one or more of the rights that are512+specifically granted under this License. You may not convey a covered513+work if you are a party to an arrangement with a third party that is514+in the business of distributing software, under which you make payment515+to the third party based on the extent of your activity of conveying516+the work, and under which the third party grants, to any of the517+parties who would receive the covered work from you, a discriminatory518+patent license (a) in connection with copies of the covered work519+conveyed by you (or copies made from those copies), or (b) primarily520+for and in connection with specific products or compilations that521+contain the covered work, unless you entered into that arrangement,522+or that patent license was granted, prior to 28 March 2007.523+524+ Nothing in this License shall be construed as excluding or limiting525+any implied license or other defenses to infringement that may526+otherwise be available to you under applicable patent law.527+528+ 12. No Surrender of Others' Freedom.529+530+ If conditions are imposed on you (whether by court order, agreement or531+otherwise) that contradict the conditions of this License, they do not532+excuse you from the conditions of this License. If you cannot convey a533+covered work so as to satisfy simultaneously your obligations under this534+License and any other pertinent obligations, then as a consequence you may535+not convey it at all. For example, if you agree to terms that obligate you536+to collect a royalty for further conveying from those to whom you convey537+the Program, the only way you could satisfy both those terms and this538+License would be to refrain entirely from conveying the Program.539+540+ 13. Remote Network Interaction; Use with the GNU General Public License.541+542+ Notwithstanding any other provision of this License, if you modify the543+Program, your modified version must prominently offer all users544+interacting with it remotely through a computer network (if your version545+supports such interaction) an opportunity to receive the Corresponding546+Source of your version by providing access to the Corresponding Source547+from a network server at no charge, through some standard or customary548+means of facilitating copying of software. This Corresponding Source549+shall include the Corresponding Source for any work covered by version 3550+of the GNU General Public License that is incorporated pursuant to the551+following paragraph.552+553+ Notwithstanding any other provision of this License, you have554+permission to link or combine any covered work with a work licensed555+under version 3 of the GNU General Public License into a single556+combined work, and to convey the resulting work. The terms of this557+License will continue to apply to the part which is the covered work,558+but the work with which it is combined will remain governed by version559+3 of the GNU General Public License.560+561+ 14. Revised Versions of this License.562+563+ The Free Software Foundation may publish revised and/or new versions of564+the GNU Affero General Public License from time to time. Such new versions565+will be similar in spirit to the present version, but may differ in detail to566+address new problems or concerns.567+568+ Each version is given a distinguishing version number. If the569+Program specifies that a certain numbered version of the GNU Affero General570+Public License "or any later version" applies to it, you have the571+option of following the terms and conditions either of that numbered572+version or of any later version published by the Free Software573+Foundation. If the Program does not specify a version number of the574+GNU Affero General Public License, you may choose any version ever published575+by the Free Software Foundation.576+577+ If the Program specifies that a proxy can decide which future578+versions of the GNU Affero General Public License can be used, that proxy's579+public statement of acceptance of a version permanently authorizes you580+to choose that version for the Program.581+582+ Later license versions may give you additional or different583+permissions. However, no additional obligations are imposed on any584+author or copyright holder as a result of your choosing to follow a585+later version.586+587+ 15. Disclaimer of Warranty.588+589+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY590+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT591+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY592+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,593+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR594+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM595+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF596+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.597+598+ 16. Limitation of Liability.599+600+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING601+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS602+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY603+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE604+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF605+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD606+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),607+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF608+SUCH DAMAGES.609+610+ 17. Interpretation of Sections 15 and 16.611+612+ If the disclaimer of warranty and limitation of liability provided613+above cannot be given local legal effect according to their terms,614+reviewing courts shall apply local law that most closely approximates615+an absolute waiver of all civil liability in connection with the616+Program, unless a warranty or assumption of liability accompanies a617+copy of the Program in return for a fee.618+619+ END OF TERMS AND CONDITIONS620+621+ How to Apply These Terms to Your New Programs622+623+ If you develop a new program, and you want it to be of the greatest624+possible use to the public, the best way to achieve this is to make it625+free software which everyone can redistribute and change under these terms.626+627+ To do so, attach the following notices to the program. It is safest628+to attach them to the start of each source file to most effectively629+state the exclusion of warranty; and each file should have at least630+the "copyright" line and a pointer to where the full notice is found.631+632+ <one line to give the program's name and a brief idea of what it does.>633+ Copyright (C) <year> <name of author>634+635+ This program is free software: you can redistribute it and/or modify636+ it under the terms of the GNU Affero General Public License as published by637+ the Free Software Foundation, either version 3 of the License, or638+ (at your option) any later version.639+640+ This program is distributed in the hope that it will be useful,641+ but WITHOUT ANY WARRANTY; without even the implied warranty of642+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the643+ GNU Affero General Public License for more details.644+645+ You should have received a copy of the GNU Affero General Public License646+ along with this program. If not, see <https://www.gnu.org/licenses/>.647+648+Also add information on how to contact you by electronic and paper mail.649+650+ If your software can interact with users remotely through a computer651+network, you should also make sure that it provides a way for users to652+get its source. For example, if your program is a web application, its653+interface could display a "Source" link that leads users to an archive654+of the code. There are many ways you could offer source, and different655+solutions will be better for different programs; see section 13 for the656+specific requirements.657+658+ You should also get your employer (if you work as a programmer) or school,659+if any, to sign a "copyright disclaimer" for the program, if necessary.660+For more information on this, and how to apply and follow the GNU AGPL, see661+<https://www.gnu.org/licenses/>.
A LICENSES/EMSCRIPTEN.txt +102 -0
@@ -0,0 +1,102 @@
1+Emscripten is available under 2 licenses, the MIT license and the2+University of Illinois/NCSA Open Source License.3+4+Both are permissive open source licenses, with little if any5+practical difference between them.6+7+The reason for offering both is that (1) the MIT license is8+well-known, while (2) the University of Illinois/NCSA Open Source9+License allows Emscripten's code to be integrated upstream into10+LLVM, which uses that license, should the opportunity arise.11+12+The full text of both licenses follows.13+14+==============================================================================15+16+Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.17+18+Permission is hereby granted, free of charge, to any person obtaining a copy19+of this software and associated documentation files (the "Software"), to deal20+in the Software without restriction, including without limitation the rights21+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell22+copies of the Software, and to permit persons to whom the Software is23+furnished to do so, subject to the following conditions:24+25+The above copyright notice and this permission notice shall be included in26+all copies or substantial portions of the Software.27+28+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR29+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,30+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE31+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER32+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,33+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN34+THE SOFTWARE.35+36+==============================================================================37+38+Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.39+All rights reserved.40+41+Permission is hereby granted, free of charge, to any person obtaining a42+copy of this software and associated documentation files (the43+"Software"), to deal with the Software without restriction, including44+without limitation the rights to use, copy, modify, merge, publish,45+distribute, sublicense, and/or sell copies of the Software, and to46+permit persons to whom the Software is furnished to do so, subject to47+the following conditions:48+49+ Redistributions of source code must retain the above copyright50+ notice, this list of conditions and the following disclaimers.51+52+ Redistributions in binary form must reproduce the above53+ copyright notice, this list of conditions and the following disclaimers54+ in the documentation and/or other materials provided with the55+ distribution.56+57+ Neither the names of Mozilla,58+ nor the names of its contributors may be used to endorse59+ or promote products derived from this Software without specific prior60+ written permission. 61+62+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS63+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF64+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.65+IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR66+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,67+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE68+SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.69+70+==============================================================================71+72+This program uses portions of Node.js source code located in src/library_path.js,73+in accordance with the terms of the MIT license. Node's license follows:74+75+ """76+ Copyright Joyent, Inc. and other Node contributors. All rights reserved.77+ Permission is hereby granted, free of charge, to any person obtaining a copy78+ of this software and associated documentation files (the "Software"), to79+ deal in the Software without restriction, including without limitation the80+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or81+ sell copies of the Software, and to permit persons to whom the Software is82+ furnished to do so, subject to the following conditions:83+84+ The above copyright notice and this permission notice shall be included in85+ all copies or substantial portions of the Software.86+87+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR88+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,89+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE90+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER91+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING92+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS93+ IN THE SOFTWARE.94+ """95+96+The musl libc project is bundled in this repo, and it has the MIT license, see97+system/lib/libc/musl/COPYRIGHT98+99+The third_party/ subdirectory contains code with other licenses. None of it is100+used by default, but certain options use it (e.g., the optional closure compiler101+flag will run closure compiler from third_party/).102+
A LICENSES/GPL-3.0.txt +674 -0
@@ -0,0 +1,674 @@
1+ GNU GENERAL PUBLIC LICENSE2+ Version 3, 29 June 20073+4+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>5+ Everyone is permitted to copy and distribute verbatim copies6+ of this license document, but changing it is not allowed.7+8+ Preamble9+10+ The GNU General Public License is a free, copyleft license for11+software and other kinds of works.12+13+ The licenses for most software and other practical works are designed14+to take away your freedom to share and change the works. By contrast,15+the GNU General Public License is intended to guarantee your freedom to16+share and change all versions of a program--to make sure it remains free17+software for all its users. We, the Free Software Foundation, use the18+GNU General Public License for most of our software; it applies also to19+any other work released this way by its authors. You can apply it to20+your programs, too.21+22+ When we speak of free software, we are referring to freedom, not23+price. Our General Public Licenses are designed to make sure that you24+have the freedom to distribute copies of free software (and charge for25+them if you wish), that you receive source code or can get it if you26+want it, that you can change the software or use pieces of it in new27+free programs, and that you know you can do these things.28+29+ To protect your rights, we need to prevent others from denying you30+these rights or asking you to surrender the rights. Therefore, you have31+certain responsibilities if you distribute copies of the software, or if32+you modify it: responsibilities to respect the freedom of others.33+34+ For example, if you distribute copies of such a program, whether35+gratis or for a fee, you must pass on to the recipients the same36+freedoms that you received. You must make sure that they, too, receive37+or can get the source code. And you must show them these terms so they38+know their rights.39+40+ Developers that use the GNU GPL protect your rights with two steps:41+(1) assert copyright on the software, and (2) offer you this License42+giving you legal permission to copy, distribute and/or modify it.43+44+ For the developers' and authors' protection, the GPL clearly explains45+that there is no warranty for this free software. For both users' and46+authors' sake, the GPL requires that modified versions be marked as47+changed, so that their problems will not be attributed erroneously to48+authors of previous versions.49+50+ Some devices are designed to deny users access to install or run51+modified versions of the software inside them, although the manufacturer52+can do so. This is fundamentally incompatible with the aim of53+protecting users' freedom to change the software. The systematic54+pattern of such abuse occurs in the area of products for individuals to55+use, which is precisely where it is most unacceptable. Therefore, we56+have designed this version of the GPL to prohibit the practice for those57+products. If such problems arise substantially in other domains, we58+stand ready to extend this provision to those domains in future versions59+of the GPL, as needed to protect the freedom of users.60+61+ Finally, every program is threatened constantly by software patents.62+States should not allow patents to restrict development and use of63+software on general-purpose computers, but in those that do, we wish to64+avoid the special danger that patents applied to a free program could65+make it effectively proprietary. To prevent this, the GPL assures that66+patents cannot be used to render the program non-free.67+68+ The precise terms and conditions for copying, distribution and69+modification follow.70+71+ TERMS AND CONDITIONS72+73+ 0. Definitions.74+75+ "This License" refers to version 3 of the GNU General Public License.76+77+ "Copyright" also means copyright-like laws that apply to other kinds of78+works, such as semiconductor masks.79+80+ "The Program" refers to any copyrightable work licensed under this81+License. Each licensee is addressed as "you". "Licensees" and82+"recipients" may be individuals or organizations.83+84+ To "modify" a work means to copy from or adapt all or part of the work85+in a fashion requiring copyright permission, other than the making of an86+exact copy. The resulting work is called a "modified version" of the87+earlier work or a work "based on" the earlier work.88+89+ A "covered work" means either the unmodified Program or a work based90+on the Program.91+92+ To "propagate" a work means to do anything with it that, without93+permission, would make you directly or secondarily liable for94+infringement under applicable copyright law, except executing it on a95+computer or modifying a private copy. Propagation includes copying,96+distribution (with or without modification), making available to the97+public, and in some countries other activities as well.98+99+ To "convey" a work means any kind of propagation that enables other100+parties to make or receive copies. Mere interaction with a user through101+a computer network, with no transfer of a copy, is not conveying.102+103+ An interactive user interface displays "Appropriate Legal Notices"104+to the extent that it includes a convenient and prominently visible105+feature that (1) displays an appropriate copyright notice, and (2)106+tells the user that there is no warranty for the work (except to the107+extent that warranties are provided), that licensees may convey the108+work under this License, and how to view a copy of this License. If109+the interface presents a list of user commands or options, such as a110+menu, a prominent item in the list meets this criterion.111+112+ 1. Source Code.113+114+ The "source code" for a work means the preferred form of the work115+for making modifications to it. "Object code" means any non-source116+form of a work.117+118+ A "Standard Interface" means an interface that either is an official119+standard defined by a recognized standards body, or, in the case of120+interfaces specified for a particular programming language, one that121+is widely used among developers working in that language.122+123+ The "System Libraries" of an executable work include anything, other124+than the work as a whole, that (a) is included in the normal form of125+packaging a Major Component, but which is not part of that Major126+Component, and (b) serves only to enable use of the work with that127+Major Component, or to implement a Standard Interface for which an128+implementation is available to the public in source code form. A129+"Major Component", in this context, means a major essential component130+(kernel, window system, and so on) of the specific operating system131+(if any) on which the executable work runs, or a compiler used to132+produce the work, or an object code interpreter used to run it.133+134+ The "Corresponding Source" for a work in object code form means all135+the source code needed to generate, install, and (for an executable136+work) run the object code and to modify the work, including scripts to137+control those activities. However, it does not include the work's138+System Libraries, or general-purpose tools or generally available free139+programs which are used unmodified in performing those activities but140+which are not part of the work. For example, Corresponding Source141+includes interface definition files associated with source files for142+the work, and the source code for shared libraries and dynamically143+linked subprograms that the work is specifically designed to require,144+such as by intimate data communication or control flow between those145+subprograms and other parts of the work.146+147+ The Corresponding Source need not include anything that users148+can regenerate automatically from other parts of the Corresponding149+Source.150+151+ The Corresponding Source for a work in source code form is that152+same work.153+154+ 2. Basic Permissions.155+156+ All rights granted under this License are granted for the term of157+copyright on the Program, and are irrevocable provided the stated158+conditions are met. This License explicitly affirms your unlimited159+permission to run the unmodified Program. The output from running a160+covered work is covered by this License only if the output, given its161+content, constitutes a covered work. This License acknowledges your162+rights of fair use or other equivalent, as provided by copyright law.163+164+ You may make, run and propagate covered works that you do not165+convey, without conditions so long as your license otherwise remains166+in force. You may convey covered works to others for the sole purpose167+of having them make modifications exclusively for you, or provide you168+with facilities for running those works, provided that you comply with169+the terms of this License in conveying all material for which you do170+not control copyright. Those thus making or running the covered works171+for you must do so exclusively on your behalf, under your direction172+and control, on terms that prohibit them from making any copies of173+your copyrighted material outside their relationship with you.174+175+ Conveying under any other circumstances is permitted solely under176+the conditions stated below. Sublicensing is not allowed; section 10177+makes it unnecessary.178+179+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.180+181+ No covered work shall be deemed part of an effective technological182+measure under any applicable law fulfilling obligations under article183+11 of the WIPO copyright treaty adopted on 20 December 1996, or184+similar laws prohibiting or restricting circumvention of such185+measures.186+187+ When you convey a covered work, you waive any legal power to forbid188+circumvention of technological measures to the extent such circumvention189+is effected by exercising rights under this License with respect to190+the covered work, and you disclaim any intention to limit operation or191+modification of the work as a means of enforcing, against the work's192+users, your or third parties' legal rights to forbid circumvention of193+technological measures.194+195+ 4. Conveying Verbatim Copies.196+197+ You may convey verbatim copies of the Program's source code as you198+receive it, in any medium, provided that you conspicuously and199+appropriately publish on each copy an appropriate copyright notice;200+keep intact all notices stating that this License and any201+non-permissive terms added in accord with section 7 apply to the code;202+keep intact all notices of the absence of any warranty; and give all203+recipients a copy of this License along with the Program.204+205+ You may charge any price or no price for each copy that you convey,206+and you may offer support or warranty protection for a fee.207+208+ 5. Conveying Modified Source Versions.209+210+ You may convey a work based on the Program, or the modifications to211+produce it from the Program, in the form of source code under the212+terms of section 4, provided that you also meet all of these conditions:213+214+ a) The work must carry prominent notices stating that you modified215+ it, and giving a relevant date.216+217+ b) The work must carry prominent notices stating that it is218+ released under this License and any conditions added under section219+ 7. This requirement modifies the requirement in section 4 to220+ "keep intact all notices".221+222+ c) You must license the entire work, as a whole, under this223+ License to anyone who comes into possession of a copy. This224+ License will therefore apply, along with any applicable section 7225+ additional terms, to the whole of the work, and all its parts,226+ regardless of how they are packaged. This License gives no227+ permission to license the work in any other way, but it does not228+ invalidate such permission if you have separately received it.229+230+ d) If the work has interactive user interfaces, each must display231+ Appropriate Legal Notices; however, if the Program has interactive232+ interfaces that do not display Appropriate Legal Notices, your233+ work need not make them do so.234+235+ A compilation of a covered work with other separate and independent236+works, which are not by their nature extensions of the covered work,237+and which are not combined with it such as to form a larger program,238+in or on a volume of a storage or distribution medium, is called an239+"aggregate" if the compilation and its resulting copyright are not240+used to limit the access or legal rights of the compilation's users241+beyond what the individual works permit. Inclusion of a covered work242+in an aggregate does not cause this License to apply to the other243+parts of the aggregate.244+245+ 6. Conveying Non-Source Forms.246+247+ You may convey a covered work in object code form under the terms248+of sections 4 and 5, provided that you also convey the249+machine-readable Corresponding Source under the terms of this License,250+in one of these ways:251+252+ a) Convey the object code in, or embodied in, a physical product253+ (including a physical distribution medium), accompanied by the254+ Corresponding Source fixed on a durable physical medium255+ customarily used for software interchange.256+257+ b) Convey the object code in, or embodied in, a physical product258+ (including a physical distribution medium), accompanied by a259+ written offer, valid for at least three years and valid for as260+ long as you offer spare parts or customer support for that product261+ model, to give anyone who possesses the object code either (1) a262+ copy of the Corresponding Source for all the software in the263+ product that is covered by this License, on a durable physical264+ medium customarily used for software interchange, for a price no265+ more than your reasonable cost of physically performing this266+ conveying of source, or (2) access to copy the267+ Corresponding Source from a network server at no charge.268+269+ c) Convey individual copies of the object code with a copy of the270+ written offer to provide the Corresponding Source. This271+ alternative is allowed only occasionally and noncommercially, and272+ only if you received the object code with such an offer, in accord273+ with subsection 6b.274+275+ d) Convey the object code by offering access from a designated276+ place (gratis or for a charge), and offer equivalent access to the277+ Corresponding Source in the same way through the same place at no278+ further charge. You need not require recipients to copy the279+ Corresponding Source along with the object code. If the place to280+ copy the object code is a network server, the Corresponding Source281+ may be on a different server (operated by you or a third party)282+ that supports equivalent copying facilities, provided you maintain283+ clear directions next to the object code saying where to find the284+ Corresponding Source. Regardless of what server hosts the285+ Corresponding Source, you remain obligated to ensure that it is286+ available for as long as needed to satisfy these requirements.287+288+ e) Convey the object code using peer-to-peer transmission, provided289+ you inform other peers where the object code and Corresponding290+ Source of the work are being offered to the general public at no291+ charge under subsection 6d.292+293+ A separable portion of the object code, whose source code is excluded294+from the Corresponding Source as a System Library, need not be295+included in conveying the object code work.296+297+ A "User Product" is either (1) a "consumer product", which means any298+tangible personal property which is normally used for personal, family,299+or household purposes, or (2) anything designed or sold for incorporation300+into a dwelling. In determining whether a product is a consumer product,301+doubtful cases shall be resolved in favor of coverage. For a particular302+product received by a particular user, "normally used" refers to a303+typical or common use of that class of product, regardless of the status304+of the particular user or of the way in which the particular user305+actually uses, or expects or is expected to use, the product. A product306+is a consumer product regardless of whether the product has substantial307+commercial, industrial or non-consumer uses, unless such uses represent308+the only significant mode of use of the product.309+310+ "Installation Information" for a User Product means any methods,311+procedures, authorization keys, or other information required to install312+and execute modified versions of a covered work in that User Product from313+a modified version of its Corresponding Source. The information must314+suffice to ensure that the continued functioning of the modified object315+code is in no case prevented or interfered with solely because316+modification has been made.317+318+ If you convey an object code work under this section in, or with, or319+specifically for use in, a User Product, and the conveying occurs as320+part of a transaction in which the right of possession and use of the321+User Product is transferred to the recipient in perpetuity or for a322+fixed term (regardless of how the transaction is characterized), the323+Corresponding Source conveyed under this section must be accompanied324+by the Installation Information. But this requirement does not apply325+if neither you nor any third party retains the ability to install326+modified object code on the User Product (for example, the work has327+been installed in ROM).328+329+ The requirement to provide Installation Information does not include a330+requirement to continue to provide support service, warranty, or updates331+for a work that has been modified or installed by the recipient, or for332+the User Product in which it has been modified or installed. Access to a333+network may be denied when the modification itself materially and334+adversely affects the operation of the network or violates the rules and335+protocols for communication across the network.336+337+ Corresponding Source conveyed, and Installation Information provided,338+in accord with this section must be in a format that is publicly339+documented (and with an implementation available to the public in340+source code form), and must require no special password or key for341+unpacking, reading or copying.342+343+ 7. Additional Terms.344+345+ "Additional permissions" are terms that supplement the terms of this346+License by making exceptions from one or more of its conditions.347+Additional permissions that are applicable to the entire Program shall348+be treated as though they were included in this License, to the extent349+that they are valid under applicable law. If additional permissions350+apply only to part of the Program, that part may be used separately351+under those permissions, but the entire Program remains governed by352+this License without regard to the additional permissions.353+354+ When you convey a copy of a covered work, you may at your option355+remove any additional permissions from that copy, or from any part of356+it. (Additional permissions may be written to require their own357+removal in certain cases when you modify the work.) You may place358+additional permissions on material, added by you to a covered work,359+for which you have or can give appropriate copyright permission.360+361+ Notwithstanding any other provision of this License, for material you362+add to a covered work, you may (if authorized by the copyright holders of363+that material) supplement the terms of this License with terms:364+365+ a) Disclaiming warranty or limiting liability differently from the366+ terms of sections 15 and 16 of this License; or367+368+ b) Requiring preservation of specified reasonable legal notices or369+ author attributions in that material or in the Appropriate Legal370+ Notices displayed by works containing it; or371+372+ c) Prohibiting misrepresentation of the origin of that material, or373+ requiring that modified versions of such material be marked in374+ reasonable ways as different from the original version; or375+376+ d) Limiting the use for publicity purposes of names of licensors or377+ authors of the material; or378+379+ e) Declining to grant rights under trademark law for use of some380+ trade names, trademarks, or service marks; or381+382+ f) Requiring indemnification of licensors and authors of that383+ material by anyone who conveys the material (or modified versions of384+ it) with contractual assumptions of liability to the recipient, for385+ any liability that these contractual assumptions directly impose on386+ those licensors and authors.387+388+ All other non-permissive additional terms are considered "further389+restrictions" within the meaning of section 10. If the Program as you390+received it, or any part of it, contains a notice stating that it is391+governed by this License along with a term that is a further392+restriction, you may remove that term. If a license document contains393+a further restriction but permits relicensing or conveying under this394+License, you may add to a covered work material governed by the terms395+of that license document, provided that the further restriction does396+not survive such relicensing or conveying.397+398+ If you add terms to a covered work in accord with this section, you399+must place, in the relevant source files, a statement of the400+additional terms that apply to those files, or a notice indicating401+where to find the applicable terms.402+403+ Additional terms, permissive or non-permissive, may be stated in the404+form of a separately written license, or stated as exceptions;405+the above requirements apply either way.406+407+ 8. Termination.408+409+ You may not propagate or modify a covered work except as expressly410+provided under this License. Any attempt otherwise to propagate or411+modify it is void, and will automatically terminate your rights under412+this License (including any patent licenses granted under the third413+paragraph of section 11).414+415+ However, if you cease all violation of this License, then your416+license from a particular copyright holder is reinstated (a)417+provisionally, unless and until the copyright holder explicitly and418+finally terminates your license, and (b) permanently, if the copyright419+holder fails to notify you of the violation by some reasonable means420+prior to 60 days after the cessation.421+422+ Moreover, your license from a particular copyright holder is423+reinstated permanently if the copyright holder notifies you of the424+violation by some reasonable means, this is the first time you have425+received notice of violation of this License (for any work) from that426+copyright holder, and you cure the violation prior to 30 days after427+your receipt of the notice.428+429+ Termination of your rights under this section does not terminate the430+licenses of parties who have received copies or rights from you under431+this License. If your rights have been terminated and not permanently432+reinstated, you do not qualify to receive new licenses for the same433+material under section 10.434+435+ 9. Acceptance Not Required for Having Copies.436+437+ You are not required to accept this License in order to receive or438+run a copy of the Program. Ancillary propagation of a covered work439+occurring solely as a consequence of using peer-to-peer transmission440+to receive a copy likewise does not require acceptance. However,441+nothing other than this License grants you permission to propagate or442+modify any covered work. These actions infringe copyright if you do443+not accept this License. Therefore, by modifying or propagating a444+covered work, you indicate your acceptance of this License to do so.445+446+ 10. Automatic Licensing of Downstream Recipients.447+448+ Each time you convey a covered work, the recipient automatically449+receives a license from the original licensors, to run, modify and450+propagate that work, subject to this License. You are not responsible451+for enforcing compliance by third parties with this License.452+453+ An "entity transaction" is a transaction transferring control of an454+organization, or substantially all assets of one, or subdividing an455+organization, or merging organizations. If propagation of a covered456+work results from an entity transaction, each party to that457+transaction who receives a copy of the work also receives whatever458+licenses to the work the party's predecessor in interest had or could459+give under the previous paragraph, plus a right to possession of the460+Corresponding Source of the work from the predecessor in interest, if461+the predecessor has it or can get it with reasonable efforts.462+463+ You may not impose any further restrictions on the exercise of the464+rights granted or affirmed under this License. For example, you may465+not impose a license fee, royalty, or other charge for exercise of466+rights granted under this License, and you may not initiate litigation467+(including a cross-claim or counterclaim in a lawsuit) alleging that468+any patent claim is infringed by making, using, selling, offering for469+sale, or importing the Program or any portion of it.470+471+ 11. Patents.472+473+ A "contributor" is a copyright holder who authorizes use under this474+License of the Program or a work on which the Program is based. The475+work thus licensed is called the contributor's "contributor version".476+477+ A contributor's "essential patent claims" are all patent claims478+owned or controlled by the contributor, whether already acquired or479+hereafter acquired, that would be infringed by some manner, permitted480+by this License, of making, using, or selling its contributor version,481+but do not include claims that would be infringed only as a482+consequence of further modification of the contributor version. For483+purposes of this definition, "control" includes the right to grant484+patent sublicenses in a manner consistent with the requirements of485+this License.486+487+ Each contributor grants you a non-exclusive, worldwide, royalty-free488+patent license under the contributor's essential patent claims, to489+make, use, sell, offer for sale, import and otherwise run, modify and490+propagate the contents of its contributor version.491+492+ In the following three paragraphs, a "patent license" is any express493+agreement or commitment, however denominated, not to enforce a patent494+(such as an express permission to practice a patent or covenant not to495+sue for patent infringement). To "grant" such a patent license to a496+party means to make such an agreement or commitment not to enforce a497+patent against the party.498+499+ If you convey a covered work, knowingly relying on a patent license,500+and the Corresponding Source of the work is not available for anyone501+to copy, free of charge and under the terms of this License, through a502+publicly available network server or other readily accessible means,503+then you must either (1) cause the Corresponding Source to be so504+available, or (2) arrange to deprive yourself of the benefit of the505+patent license for this particular work, or (3) arrange, in a manner506+consistent with the requirements of this License, to extend the patent507+license to downstream recipients. "Knowingly relying" means you have508+actual knowledge that, but for the patent license, your conveying the509+covered work in a country, or your recipient's use of the covered work510+in a country, would infringe one or more identifiable patents in that511+country that you have reason to believe are valid.512+513+ If, pursuant to or in connection with a single transaction or514+arrangement, you convey, or propagate by procuring conveyance of, a515+covered work, and grant a patent license to some of the parties516+receiving the covered work authorizing them to use, propagate, modify517+or convey a specific copy of the covered work, then the patent license518+you grant is automatically extended to all recipients of the covered519+work and works based on it.520+521+ A patent license is "discriminatory" if it does not include within522+the scope of its coverage, prohibits the exercise of, or is523+conditioned on the non-exercise of one or more of the rights that are524+specifically granted under this License. You may not convey a covered525+work if you are a party to an arrangement with a third party that is526+in the business of distributing software, under which you make payment527+to the third party based on the extent of your activity of conveying528+the work, and under which the third party grants, to any of the529+parties who would receive the covered work from you, a discriminatory530+patent license (a) in connection with copies of the covered work531+conveyed by you (or copies made from those copies), or (b) primarily532+for and in connection with specific products or compilations that533+contain the covered work, unless you entered into that arrangement,534+or that patent license was granted, prior to 28 March 2007.535+536+ Nothing in this License shall be construed as excluding or limiting537+any implied license or other defenses to infringement that may538+otherwise be available to you under applicable patent law.539+540+ 12. No Surrender of Others' Freedom.541+542+ If conditions are imposed on you (whether by court order, agreement or543+otherwise) that contradict the conditions of this License, they do not544+excuse you from the conditions of this License. If you cannot convey a545+covered work so as to satisfy simultaneously your obligations under this546+License and any other pertinent obligations, then as a consequence you may547+not convey it at all. For example, if you agree to terms that obligate you548+to collect a royalty for further conveying from those to whom you convey549+the Program, the only way you could satisfy both those terms and this550+License would be to refrain entirely from conveying the Program.551+552+ 13. Use with the GNU Affero General Public License.553+554+ Notwithstanding any other provision of this License, you have555+permission to link or combine any covered work with a work licensed556+under version 3 of the GNU Affero General Public License into a single557+combined work, and to convey the resulting work. The terms of this558+License will continue to apply to the part which is the covered work,559+but the special requirements of the GNU Affero General Public License,560+section 13, concerning interaction through a network will apply to the561+combination as such.562+563+ 14. Revised Versions of this License.564+565+ The Free Software Foundation may publish revised and/or new versions of566+the GNU General Public License from time to time. Such new versions will567+be similar in spirit to the present version, but may differ in detail to568+address new problems or concerns.569+570+ Each version is given a distinguishing version number. If the571+Program specifies that a certain numbered version of the GNU General572+Public License "or any later version" applies to it, you have the573+option of following the terms and conditions either of that numbered574+version or of any later version published by the Free Software575+Foundation. If the Program does not specify a version number of the576+GNU General Public License, you may choose any version ever published577+by the Free Software Foundation.578+579+ If the Program specifies that a proxy can decide which future580+versions of the GNU General Public License can be used, that proxy's581+public statement of acceptance of a version permanently authorizes you582+to choose that version for the Program.583+584+ Later license versions may give you additional or different585+permissions. However, no additional obligations are imposed on any586+author or copyright holder as a result of your choosing to follow a587+later version.588+589+ 15. Disclaimer of Warranty.590+591+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY592+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT593+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY594+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,595+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR596+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM597+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF598+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.599+600+ 16. Limitation of Liability.601+602+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING603+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS604+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY605+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE606+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF607+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD608+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),609+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF610+SUCH DAMAGES.611+612+ 17. Interpretation of Sections 15 and 16.613+614+ If the disclaimer of warranty and limitation of liability provided615+above cannot be given local legal effect according to their terms,616+reviewing courts shall apply local law that most closely approximates617+an absolute waiver of all civil liability in connection with the618+Program, unless a warranty or assumption of liability accompanies a619+copy of the Program in return for a fee.620+621+ END OF TERMS AND CONDITIONS622+623+ How to Apply These Terms to Your New Programs624+625+ If you develop a new program, and you want it to be of the greatest626+possible use to the public, the best way to achieve this is to make it627+free software which everyone can redistribute and change under these terms.628+629+ To do so, attach the following notices to the program. It is safest630+to attach them to the start of each source file to most effectively631+state the exclusion of warranty; and each file should have at least632+the "copyright" line and a pointer to where the full notice is found.633+634+ <one line to give the program's name and a brief idea of what it does.>635+ Copyright (C) <year> <name of author>636+637+ This program is free software: you can redistribute it and/or modify638+ it under the terms of the GNU General Public License as published by639+ the Free Software Foundation, either version 3 of the License, or640+ (at your option) any later version.641+642+ This program is distributed in the hope that it will be useful,643+ but WITHOUT ANY WARRANTY; without even the implied warranty of644+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the645+ GNU General Public License for more details.646+647+ You should have received a copy of the GNU General Public License648+ along with this program. If not, see <http://www.gnu.org/licenses/>.649+650+Also add information on how to contact you by electronic and paper mail.651+652+ If the program does terminal interaction, make it output a short653+notice like this when it starts in an interactive mode:654+655+ <program> Copyright (C) <year> <name of author>656+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.657+ This is free software, and you are welcome to redistribute it658+ under certain conditions; type `show c' for details.659+660+The hypothetical commands `show w' and `show c' should show the appropriate661+parts of the General Public License. Of course, your program's commands662+might be different; for a GUI interface, you would use an "about box".663+664+ You should also get your employer (if you work as a programmer) or school,665+if any, to sign a "copyright disclaimer" for the program, if necessary.666+For more information on this, and how to apply and follow the GNU GPL, see667+<http://www.gnu.org/licenses/>.668+669+ The GNU General Public License does not permit incorporating your program670+into proprietary programs. If your program is a subroutine library, you671+may consider it more useful to permit linking proprietary applications with672+the library. If this is what you want to do, use the GNU Lesser General673+Public License instead of this License. But first, please read674+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
A NOTICE.md +31 -0
@@ -0,0 +1,31 @@
1+# gud.pics — source, licenses, and changes2+3+Independent browser prototype. Not affiliated with or endorsed by darktable or Adobe.4+5+## Source provenance6+7+Vendored darktable revision: `0286264e169eec79ce8d3744d6e477dcb18d9d94`.8+Repository: https://github.com/darktable-org/darktable9+10+- `native/upstream/ashift.c`: darktable developers, GPL-3.0-or-later.11+- `native/upstream/ashift_lsd.c`: darktable integration, GPL-3.0-or-later; embedded LSD 1.6 by Rafael Grompone von Gioi et al., AGPL-3.0-or-later.12+- `native/upstream/ashift_nmsimplex.c`: darktable integration, GPL-3.0-or-later; embedded Nelder–Mead implementation by Michael F. Hutt, MIT-style permission notice retained in full.13+- Emscripten-generated runtime: Emscripten authors, MIT / University of Illinois/NCSA; see `LICENSES/EMSCRIPTEN.txt`.14+- New adapter, browser code, crop algorithm, and build scripts: AGPL-3.0-or-later. Provided without warranty.15+16+Original notices are retained in the source. Both GPL and AGPL license texts are supplied. GPLv3 §13 and AGPLv3 §13 address their combination. The public repository at https://github.com/FelineStateMachine/gudpics contains the preferred editable forms and build instructions; keep it available when distributing this combined work. No paid or proprietary assets are used.17+18+## Extraction changes (2026-09-21)19+20+`scripts/extract.py` reproducibly selects darktable's original constants, line classification, vector helpers, homography, RANSAC, model fitness and Nelder–Mead fit from the pinned source. It uses the original LSD detector and simplex implementation.21+22+Changes: remove GTK, image pipeline and OpenMP dependencies; supply portable math helpers; keep minimal fit state behind a C API; accept display-referred RGBA rather than RAW; omit the optional detail and edge enhancement preprocessing; replace darktable logging in LSD with stderr. Keep one/two-line selections when RANSAC is skipped. Seed RANSAC deterministically. Add input and finite-result checks at the browser boundary. The generic 28 mm lens model is used, without EXIF focal-length inference.23+24+Browser preview/export uses a new inverse-mapped bilinear renderer. Automatic crop uses a new conservative polygon scan; it does not reproduce darktable's crop optimizer. Analysis is limited to 960 pixels on the long edge; export to 2560 pixels and 4 megapixels. The browser supplies decoding, orientation, and canvas color conversion. Results are not claimed to be pixel-identical to darktable or Lightroom.25+26+## Source references27+28+- Module: https://github.com/darktable-org/darktable/blob/0286264e169eec79ce8d3744d6e477dcb18d9d94/src/iop/ashift.c29+- Detector: https://github.com/darktable-org/darktable/blob/0286264e169eec79ce8d3744d6e477dcb18d9d94/src/iop/ashift_lsd.c30+- Optimizer: https://github.com/darktable-org/darktable/blob/0286264e169eec79ce8d3744d6e477dcb18d9d94/src/iop/ashift_nmsimplex.c31+- Manual: https://darktable-org.github.io/dtdocs/en/module-reference/processing-modules/rotate-perspective/
A README.md +61 -0
@@ -0,0 +1,61 @@
1+# gud.pics2+3+A mobile-first, installable photo toolkit. Source: https://github.com/FelineStateMachine/gudpics Every tool runs locally in the browser; Cloudflare serves static files and photos never leave the device.4+5+Tools:6+7+- `/perspective`: straightens converging verticals and horizontals using darktable's C algorithms (LSD line detection, RANSAC, perspective fit, homography) compiled to WebAssembly.8+9+## Run10+11+The checked-in `assets/core.mjs` and `assets/core.wasm` are ready to use.12+13+```sh14+python3 scripts/package.py15+python3 scripts/serve.py 876516+```17+18+Open http://localhost:8765. The dev server maps extensionless routes such as `/perspective` to `perspective.html`, matching Cloudflare's asset handling. Service workers require localhost or HTTPS.19+20+## Rebuild21+22+Requires Python 3, Emscripten (`emcc`, built here with 6.0.9), Node.js for tests, and Pillow only to regenerate icons. No network is needed for extraction.23+24+```sh25+sh scripts/build.sh26+node scripts/test-core.mjs27+python3 scripts/package.py28+```29+30+If Emscripten's installed cache is read-only, set `EM_CACHE` to a writable directory when running the build.31+32+`scripts/extract.py` documents each source adaptation. Original source at the pinned revision is in `native/upstream`; retain original notices. See NOTICE.md and LICENSES before redistributing.33+34+## Cloudflare deployment35+36+```sh37+npx wrangler@4 deploy --dry-run38+npx wrangler@4 deploy39+```40+41+The config serves only `dist/`; it has no backend, secrets, storage bindings, analytics, photo upload endpoint, or runtime npm dependencies. Re-run packaging after changes: the service-worker version derives from the bundled files. Existing open tabs finish using their cached version; close all app tabs and reopen to activate an update.42+43+## Layout44+45+- `index.html` + `home.mjs`: landing grid of tools, install and about.46+- `perspective.html` + `app.mjs`: the perspective tool UI. `worker.mjs` runs the WebAssembly core off the main thread.47+- `style.css`: shared styles. Dark, system font, safe-area aware; the tool page never scrolls.48+49+## Perspective tool50+51+Photo import; original orientation via browser decoding; LSD structure detection; RANSAC outlier rejection; vertical, horizontal, or both-axis fit (including shear for both); color-coded accepted/rejected line overlays and counts; rotation and correction strength; crop modes; grid; before/after (tap the compare button or press and hold the photo); JPG/PNG export; install manifest and offline cache.52+53+54+## Prototype limits55+56+- JPEG/PNG only, 60 MB file limit. Imported images are flattened onto white and reduced to at most 4 MP / 2560 px. Analysis uses a 960 px preview.57+- Browser 8-bit color handling; exports omit EXIF and source ICC metadata. No RAW or lens distortion profiles.58+- Generic lens model, approximate crop, bilinear resampling. Not a full darktable pipeline or a reproduction of Lightroom's proprietary Auto mode.59+- No manual line drawing/selection yet. Overlay colors: green vertical accepted, blue horizontal accepted, red vertical rejected, yellow horizontal rejected, gray other edges.60+- Real-device iOS/Android memory, installation, photo picker, and sharing behavior still need hands-on validation. A desktop browser with a phone-sized viewport cannot establish those guarantees.61+- LSD's inherited error routine can terminate its WASM instance on allocation failure; the UI catches worker errors, but reload may be required after memory exhaustion.
A _headers +16 -0
A app.mjs +157 -0
@@ -0,0 +1,157 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later2+import {project} from './geometry.mjs';3+const $=id=>document.getElementById(id);4+const worker=new Worker(new URL('./worker.mjs',import.meta.url),{type:'module'});5+let nextId=0;const pending=new Map();6+worker.onmessage=({data})=>{const p=pending.get(data.id);if(!p)return;pending.delete(data.id);data.error?p.reject(Error(data.error)):p.resolve(data.result);};7+worker.onerror=()=>{for(const p of pending.values())p.reject(Error('The photo engine stopped. Reload to try again.'));pending.clear();};8+function rpc(type,args={},transfer=[]) {return new Promise((resolve,reject)=>{const id=++nextId;pending.set(id,{resolve,reject});worker.postMessage({type,id,...args},transfer);});}9+10+const tool=$('tool'),canvas=$('canvas'),overlay=$('overlay'),ctx=canvas.getContext('2d');11+let original=null,rendered=null,lines=[],analysisSize=null,base=[0,0,0,0],activeMode=null,showBefore=false,holdBefore=false,busy=false,hasDetected=false,renderId=0;12+let filename='photo',lastRender=null,renderTimer,cropMode='largest',showLines=true,showGuides=false;13+const fitButtons=[...document.querySelectorAll('[data-mode]')];14+15+let toastTimer;16+function status(message,error=false){clearTimeout(toastTimer);const el=$('status');el.textContent=message;el.classList.toggle('error',error);el.hidden=!message;if(message)toastTimer=setTimeout(()=>{el.hidden=true;},error?5000:2500);}17+function syncControls(){18+ const ready=Boolean(original)&&!busy;19+ tool.dataset.state=!original?'empty':hasDetected?'detected':'loaded';20+ for(const id of ['detect','rotation','crop','reset','save','lines','guides'])$(id).disabled=!ready;21+ $('open').disabled=busy;$('open-empty').disabled=busy;22+ $('strength').disabled=!ready||activeMode===null;23+ $('strength-row').classList.toggle('disabled',$('strength').disabled);24+ $('rotation-row').classList.toggle('disabled',!ready);25+ $('compare').disabled=!ready||!rendered;26+ fitButtons.forEach(b=>{b.disabled=!ready||!hasDetected;b.setAttribute('aria-pressed',Number(b.dataset.mode)===activeMode?'true':'false');});27+ $('lines').setAttribute('aria-pressed',String(showLines));$('guides').setAttribute('aria-pressed',String(showGuides));28+}29+function setBusy(value) {busy=value;$('busy').hidden=!value;syncControls();}30+function currentParams(){const s=Number($('strength').value)/100;return [Number($('rotation').value),base[1]*s,base[2]*s,base[3]*s];}31+function updateLabels(){const r=Number($('rotation').value);$('rotation-value').value=`${r>0?'+':''}${r.toFixed(2)}°`;$('strength-value').value=`${$('strength').value}%`;}32+function draw(){33+ const before=showBefore||holdBefore,source=before||!rendered?original:rendered;34+ if(!source)return;35+ canvas.width=source.width;canvas.height=source.height;ctx.drawImage(source,0,0);36+ $('compare').setAttribute('aria-pressed',String(before&&Boolean(rendered)));37+ drawOverlay();38+}39+function drawOverlay(){40+ overlay.width=canvas.width;overlay.height=canvas.height;41+ const c=overlay.getContext('2d'),scale=canvas.width/Math.max(canvas.clientWidth,1),before=showBefore||holdBefore;42+ if(showGuides){c.strokeStyle='#fff9';c.lineWidth=scale*.75;c.beginPath();for(let i=1;i<4;i++){c.moveTo(i*canvas.width/4,0);c.lineTo(i*canvas.width/4,canvas.height);c.moveTo(0,i*canvas.height/4);c.lineTo(canvas.width,i*canvas.height/4);}c.stroke();}43+ if(!showLines||!analysisSize)return;44+ const transform=(x,y)=>{45+ if(before||!rendered||!lastRender)return [x/analysisSize.w*canvas.width,y/analysisSize.h*canvas.height];46+ const r=lastRender,p=project(r.matrix,x/analysisSize.w*r.sourceWidth,y/analysisSize.h*r.sourceHeight);47+ return [(p[0]-r.crop.x)/r.crop.w*canvas.width,(p[1]-r.crop.y)/r.crop.h*canvas.height];48+ };49+ const rejectedToo=$('show-rejected').checked;50+ for(let i=0;i<lines.length;i+=6){51+ const type=lines[i+4],selected=type&4,vertical=type&2;52+ if(!selected&&!rejectedToo)continue;53+ const color=!(type&1)?'#8d979a':vertical?(selected?'#6af074':'#ff706b'):(selected?'#5e9dff':'#ffe15c');54+ try{const a=transform(lines[i],lines[i+1]),b=transform(lines[i+2],lines[i+3]);c.beginPath();c.moveTo(...a);c.lineTo(...b);c.strokeStyle='#00000066';c.lineWidth=3*scale;c.stroke();c.strokeStyle=color;c.lineWidth=1.25*scale;c.stroke();}catch{}55+ }56+}57+function scaledCanvas(source,max){const c=document.createElement('canvas');const s=Math.min(1,max/Math.max(source.width,source.height),Math.sqrt(4e6/(source.width*source.height)));c.width=Math.max(2,Math.round(source.width*s));c.height=Math.max(2,Math.round(source.height*s));const x=c.getContext('2d');x.fillStyle='white';x.fillRect(0,0,c.width,c.height);x.drawImage(source,0,0,c.width,c.height);return c;}58+const pixels=c=>({w:c.width,h:c.height,pixels:c.getContext('2d').getImageData(0,0,c.width,c.height).data});59+function resetValues(){base=[0,0,0,0];activeMode=null;showBefore=false;rendered=null;lastRender=null;$('rotation').value=0;$('strength').value=100;updateLabels();}60+async function load(source,name){61+ clearTimeout(renderTimer);renderId++;resetValues();hasDetected=false;lines=[];analysisSize=null;62+ filename=name.replace(/\.[^.]+$/,'');63+ const full=scaledCanvas(source,2560);original=scaledCanvas(full,1200);64+ const a=pixels(full),b=pixels(original);65+ await rpc('load',{photo:a,preview:b},[a.pixels.buffer,b.pixels.buffer]);66+ $('line-count').textContent='';67+ draw();status('');68+}69+async function detect(){70+ setBusy(true);71+ try{72+ const a=pixels(scaledCanvas(original,960)),start=performance.now();73+ const result=await rpc('detect',{...a},[a.pixels.buffer]);74+ lines=result.lines;analysisSize={w:result.w,h:result.h};hasDetected=true;showLines=true;75+ let v=0,h=0,rejected=0,other=0;76+ for(let i=4;i<lines.length;i+=6){const t=lines[i];if(!(t&1))other++;else if(!(t&4))rejected++;else if(t&2)v++;else h++;}77+ $('line-count').textContent=`${v+h} lines`;78+ $('debug-counts').textContent=`${v} vertical and ${h} horizontal lines used. ${rejected} rejected, ${other} other edges, ${lines.length/6} found in ${Math.round(performance.now()-start)} ms.`;79+ status(v+h>=2?'':'Few usable lines. Try clearer straight edges.',v+h<2);drawOverlay();80+ }catch(e){hasDetected=false;status(e.message,true);}finally{setBusy(false);}81+}82+async function render(exporting=false){83+ const id=++renderId;84+ const result=await rpc('render',{params:currentParams(),crop:cropMode,export:exporting});85+ const c=document.createElement('canvas');c.width=result.w;c.height=result.h;c.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(result.pixels),result.w,result.h),0,0);86+ if(exporting)return c;87+ if(id!==renderId)return;88+ rendered=c;lastRender=result;showBefore=false;draw();syncControls();89+}90+async function fit(mode){91+ clearTimeout(renderTimer);setBusy(true);92+ const old={base:base.slice(),mode:activeMode,rotation:$('rotation').value};93+ try{const r=await rpc('fit',{mode});base=r.params;activeMode=mode;$('rotation').value=base[0];$('strength').value=100;updateLabels();await render();}94+ catch(e){base=old.base;activeMode=old.mode;$('rotation').value=old.rotation;updateLabels();status(e.message,true);}finally{setBusy(false);}95+}96+function scheduleRender(){updateLabels();clearTimeout(renderTimer);renderTimer=setTimeout(()=>{render().catch(e=>status(e.message,true));},100);}97+98+$('detect').onclick=detect;fitButtons.forEach(b=>b.onclick=()=>fit(Number(b.dataset.mode)));99+$('rotation').oninput=scheduleRender;$('strength').oninput=scheduleRender;100+$('lines').onclick=()=>{showLines=!showLines;syncControls();drawOverlay();};101+$('guides').onclick=()=>{showGuides=!showGuides;syncControls();drawOverlay();};102+$('show-rejected').onchange=drawOverlay;103+$('compare').onclick=()=>{showBefore=!showBefore;draw();};104+$('reset').onclick=()=>{clearTimeout(renderTimer);renderId++;resetValues();cropMode='largest';syncCrop();draw();syncControls();status('');};105+106+// Press and hold the photo to peek at the original.107+const stage=$('stage');108+stage.addEventListener('pointerdown',e=>{if(!rendered||busy||e.target.closest('button'))return;holdBefore=true;draw();});109+for(const type of ['pointerup','pointercancel','pointerleave'])stage.addEventListener(type,()=>{if(!holdBefore)return;holdBefore=false;draw();});110+stage.addEventListener('contextmenu',e=>e.preventDefault());111+// Drag and drop onto the stage.112+const zone=$('open-empty');113+stage.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';zone.classList.add('over');});114+stage.addEventListener('dragleave',()=>zone.classList.remove('over'));115+stage.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('over');const file=e.dataTransfer.files[0];if(file)openFile(file);});116+117+// Sheets118+function sheet(id){const d=$(id);d.onclick=e=>{if(e.target===d||e.target.closest('[data-close]'))d.close();};return d;}119+const cropSheet=sheet('crop-sheet'),saveSheet=sheet('save-sheet'),linesSheet=sheet('lines-sheet');120+function syncCrop(){for(const b of cropSheet.querySelectorAll('[data-crop]'))b.setAttribute('aria-checked',String(b.dataset.crop===cropMode));}121+$('crop').onclick=()=>cropSheet.showModal();122+for(const b of cropSheet.querySelectorAll('[data-crop]'))b.onclick=()=>{cropMode=b.dataset.crop;syncCrop();cropSheet.close();if(rendered)scheduleRender();};123+$('line-chip').onclick=()=>linesSheet.showModal();124+$('save').onclick=()=>saveSheet.showModal();125+for(const b of saveSheet.querySelectorAll('[data-format]'))b.onclick=()=>{saveSheet.close();save(b.dataset.format);};126+127+$('open').onclick=()=>$('file').click();$('open-empty').onclick=()=>$('file').click();128+$('file').onchange=()=>{const file=$('file').files[0];if(file)openFile(file);};129+async function openFile(file){130+ if(busy)return;131+ setBusy(true);132+ let bitmap;133+ try{134+ if(file.size>60*1024*1024)throw Error('Choose a JPEG or PNG smaller than 60 MB.');135+ const head=new Uint8Array(await file.slice(0,8).arrayBuffer());136+ if(!((head[0]===255&&head[1]===216)||(head[0]===137&&head[1]===80&&head[2]===78&&head[3]===71)))throw Error('Only JPEG and PNG photos are supported.');137+ bitmap=await createImageBitmap(file,{imageOrientation:'from-image'});138+ if(bitmap.width<8||bitmap.height<8)throw Error('Choose an image at least 8 by 8 pixels.');139+ const originalWidth=bitmap.width,originalHeight=bitmap.height;140+ await load(bitmap,file.name);141+ if(originalWidth>2560||originalHeight>2560||originalWidth*originalHeight>4e6)status('Reduced to 4 MP');142+ }catch(e){status(e.message||'This image could not be opened.',true);}finally{bitmap?.close();$('file').value='';setBusy(false);}143+};144+async function save(format){145+ clearTimeout(renderTimer);setBusy(true);146+ try{147+ const c=await render(true);148+ const blob=await new Promise(resolve=>c.toBlob(resolve,`image/${format}`,0.94));149+ if(!blob)throw Error('Save failed. Try a smaller photo.');150+ const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=`${filename}-perspective.${format==='jpeg'?'jpg':'png'}`;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);151+ status(`Saved ${c.width} x ${c.height}`);152+ }catch(e){status(e.message,true);}finally{setBusy(false);}153+}154+new ResizeObserver(drawOverlay).observe(canvas);155+if('serviceWorker'in navigator)navigator.serviceWorker.register('./sw.js').catch(()=>{});156+syncCrop();157+try{await rpc('init');}catch(e){status(e.message,true);}finally{setBusy(false);}
A assets/core.mjs +2 -0
@@ -0,0 +1,2 @@
1+async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["f"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("core.wasm")}return new URL("core.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var HEAPU8;var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);var getHeapMax=()=>268435456;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var HEAPU32;var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}var _malloc,_free,_detect,_lines,_fit,_matrix,_parameters,_warp,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_malloc=Module["_malloc"]=wasmExports["g"];_free=Module["_free"]=wasmExports["h"];_detect=Module["_detect"]=wasmExports["i"];_lines=Module["_lines"]=wasmExports["j"];_fit=Module["_fit"]=wasmExports["k"];_matrix=Module["_matrix"]=wasmExports["l"];_parameters=Module["_parameters"]=wasmExports["m"];_warp=Module["_warp"]=wasmExports["n"];memory=wasmMemory=wasmExports["e"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={c:___assert_fail,b:_emscripten_resize_heap,d:_exit,a:_fd_write};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();2+;return Module}export default Module;
A assets/core.wasm +0 -0

Binary file.

A assets/icon-192.png +0 -0

Binary file.

A assets/icon-512.png +0 -0

Binary file.

A assets/icon.svg +1 -0
A geometry.mjs +50 -0
@@ -0,0 +1,50 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later2+export function project(m, x, y) {3+ const z=m[6]*x+m[7]*y+m[8];4+ if(!Number.isFinite(z)||z<=1e-6) throw Error('This correction folds the image. Reduce the strength.');5+ return [(m[0]*x+m[1]*y+m[2])/z,(m[3]*x+m[4]*y+m[5])/z];6+}7+export function inverse(m) {8+ const [a,b,c,d,e,f,g,h,i]=m;9+ const det=a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g);10+ if(!Number.isFinite(det)||Math.abs(det)<1e-12) throw Error('The correction is too extreme.');11+ return [e*i-f*h,c*h-b*i,b*f-c*e,f*g-d*i,a*i-c*g,c*d-a*f,d*h-e*g,b*g-a*h,a*e-b*d].map(v=>v/det);12+}13+// Conservative scan of a convex projected image. Rows use the intersection14+// at both ends, so every point of the returned rectangle is inside the polygon.15+// This is a new browser crop implementation, not darktable's crop optimizer.16+export function cropRect(m,w,h,mode='largest') {17+ const q=[[1,1],[w-2,1],[w-2,h-2],[1,h-2]].map(p=>project(m,...p));18+ const xs=q.map(p=>p[0]),ys=q.map(p=>p[1]);19+ const left=Math.min(...xs),right=Math.max(...xs),top=Math.min(...ys),bottom=Math.max(...ys);20+ if(![left,right,top,bottom].every(Number.isFinite)||(right-left)*(bottom-top)>w*h*5) throw Error('This correction stretches the image too far.');21+ if(mode==='none') return {x:left,y:top,w:right-left,h:bottom-top};22+ const interval=y=>{23+ const values=[];24+ for(let i=0;i<4;i++) {25+ const a=q[i],b=q[(i+1)%4];26+ if(Math.abs(a[1]-b[1])<1e-8) { if(Math.abs(y-a[1])<1e-6) values.push(a[0],b[0]); }27+ else if(y>=Math.min(a[1],b[1])-1e-6&&y<=Math.max(a[1],b[1])+1e-6)28+ values.push(a[0]+(y-a[1])*(b[0]-a[0])/(b[1]-a[1]));29+ }30+ return values.length?[Math.min(...values),Math.max(...values)]:[Infinity,-Infinity];31+ };32+ const n=256,dy=(bottom-top)/n,rows=[];33+ for(let i=0;i<n;i++) {34+ const a=interval(top+i*dy),b=interval(top+(i+1)*dy);35+ rows.push([Math.max(a[0],b[0]),Math.min(a[1],b[1])]);36+ }37+ let best=null,area=0;38+ for(let i=0;i<n;i++) {39+ let l=-Infinity,r=Infinity;40+ for(let j=i;j<n;j++) {41+ l=Math.max(l,rows[j][0]); r=Math.min(r,rows[j][1]);42+ if(r<=l) break;43+ let cw=r-l,ch=(j-i+1)*dy;44+ if(mode==='original') {cw=Math.min(cw,ch*w/h);ch=cw*h/w;}45+ if(cw*ch>area) {area=cw*ch;best={x:(l+r-cw)/2,y:top+i*dy+((j-i+1)*dy-ch)/2,w:cw,h:ch};}46+ }47+ }48+ if(!best||area<w*h*.08) throw Error('Too little image remains. Try a gentler correction.');49+ return best;50+}
A home.mjs +15 -0
A index.html +62 -0
@@ -0,0 +1,62 @@
1+<!doctype html>2+<html lang="en">3+<head>4+ <meta charset="utf-8">5+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">6+ <meta name="theme-color" content="#000000">7+ <meta name="description" content="Photo tools that run on your phone. Nothing is uploaded.">8+ <meta name="apple-mobile-web-app-capable" content="yes">9+ <meta name="mobile-web-app-capable" content="yes">10+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">11+ <meta name="apple-mobile-web-app-title" content="gud.pics">12+ <link rel="manifest" href="./manifest.webmanifest">13+ <link rel="icon" href="./assets/icon.svg" type="image/svg+xml">14+ <link rel="apple-touch-icon" href="./assets/icon-192.png">15+ <link rel="stylesheet" href="./style.css">16+ <title>gud.pics</title>17+</head>18+<body>19+ <main class="home">20+ <h1 class="home-title">gud.pics</h1>21+22+ <ul class="tools">23+ <li>24+ <a class="tool-card" href="./perspective">25+ <svg viewBox="0 0 160 96" aria-hidden="true">26+ <path d="M42 14 L118 14 L138 82 L22 82 Z" fill="none" stroke="#5e5e5e" stroke-width="4" stroke-linejoin="round"/>27+ <path d="M60 14 L100 14 M52 40 L108 40 M46 62 L114 62" stroke="#5e5e5e" stroke-width="3" stroke-linecap="round"/>28+ <rect x="42" y="14" width="76" height="68" rx="3" fill="none" stroke="#f0c04a" stroke-width="4"/>29+ <path d="M42 40 L118 40 M42 62 L118 62" stroke="#f0c04a" stroke-width="3" stroke-linecap="round"/>30+ </svg>31+ <h2>Perspective</h2>32+ </a>33+ </li>34+ </ul>35+36+ <section class="list" aria-label="App">37+ <div class="rows">38+ <button class="row" id="install" hidden>Install<svg class="chev" width="8" height="14" viewBox="0 0 8 14" aria-hidden="true"><path d="M1 1l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>39+ <button class="row" id="install-ios" hidden>Add to Home Screen<svg class="chev" width="8" height="14" viewBox="0 0 8 14" aria-hidden="true"><path d="M1 1l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>40+ <button class="row" id="about">About<svg class="chev" width="8" height="14" viewBox="0 0 8 14" aria-hidden="true"><path d="M1 1l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>41+ <a class="row" href="https://github.com/FelineStateMachine/gudpics" target="_blank" rel="noopener">Source<svg class="chev" width="8" height="14" viewBox="0 0 8 14" aria-hidden="true"><path d="M1 1l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></a>42+ </div>43+ </section>44+ </main>45+46+ <dialog id="info" class="sheet" aria-labelledby="info-title">47+ <div class="sheet-body" tabindex="-1" autofocus>48+ <div class="grabber"></div>49+ <div class="sheet-head"><h2 id="info-title">About</h2><button class="sheet-close" id="close-info">Done</button></div>50+ <p>Runs on your device. Nothing is uploaded. <span id="offline-status"></span></p>51+ <p>JPEG and PNG, up to 4 MP and 2560 px. Saved copies drop EXIF and color profiles.</p>52+ <h3>Install</h3>53+ <p id="install-help">iPhone: Safari, Share, Add to Home Screen. Android: Install app.</p>54+ <h3>Credits</h3>55+ <p>Built on darktable's perspective code, compiled to WebAssembly. Independent, not affiliated.</p>56+ <div class="links"><a href="https://github.com/FelineStateMachine/gudpics" target="_blank" rel="noopener">Source</a><a href="./NOTICE.md">Notice</a><a href="./LICENSES/AGPL-3.0.txt">AGPL</a><a href="./LICENSES/GPL-3.0.txt">GPL</a></div>57+ </div>58+ </dialog>59+60+ <script type="module" src="./home.mjs"></script>61+</body>62+</html>
A manifest.webmanifest +13 -0
A native/compat.h +41 -0
@@ -0,0 +1,41 @@
1+// Portable math support for the darktable extraction.2+// SPDX-License-Identifier: AGPL-3.0-or-later3+#include <assert.h>4+#include <float.h>5+#include <limits.h>6+#include <math.h>7+#include <stdint.h>8+#include <stdio.h>9+#include <stdlib.h>10+#include <string.h>11+#define DT_ALIGNED_PIXEL12+#define DT_ALIGNED_ARRAY13+#define DT_OMP_FOR()14+#define TRUE 115+#define FALSE 016+#define MIN(a,b) ((a)<(b)?(a):(b))17+#define MAX(a,b) ((a)>(b)?(a):(b))18+#define CLAMP(x,a,b) MIN(MAX((x),(a)),(b))19+#define dt_isnan isnan20+#define dt_fast_hypotf hypotf21+typedef int gboolean;22+static double deg2rad(double x) { return x * M_PI / 180.0; }23+static float deg2radf(float x) { return x * (float)M_PI / 180.0f; }24+static float rad2degf(float x) { return x * 180.0f / (float)M_PI; }25+static void mat3mul(float *out, const float *a, const float *b) {26+ float t[9] = {0};27+ for(int i=0;i<3;i++) for(int j=0;j<3;j++) for(int k=0;k<3;k++) t[i*3+j] += a[i*3+k]*b[k*3+j];28+ memcpy(out,t,sizeof(t));29+}30+static void mat3mulv(float *out, const float *a, const float *b) {31+ for(int i=0;i<3;i++) out[i]=a[i*3]*b[0]+a[i*3+1]*b[1]+a[i*3+2]*b[2];32+}33+static int mat3inv(float *o, const float *m) {34+ const float d=m[0]*(m[4]*m[8]-m[5]*m[7])-m[1]*(m[3]*m[8]-m[5]*m[6])+m[2]*(m[3]*m[7]-m[4]*m[6]);35+ if(fabsf(d)<1e-15f) return 1;36+ float t[9]={m[4]*m[8]-m[5]*m[7],m[2]*m[7]-m[1]*m[8],m[1]*m[5]-m[2]*m[4],37+ m[5]*m[6]-m[3]*m[8],m[0]*m[8]-m[2]*m[6],m[2]*m[3]-m[0]*m[5],38+ m[3]*m[7]-m[4]*m[6],m[1]*m[6]-m[0]*m[7],m[0]*m[4]-m[1]*m[3]};39+ for(int i=0;i<9;i++) o[i]=t[i]/d;40+ return 0;41+}
A native/core.c +76 -0
@@ -0,0 +1,76 @@
1+// Upright browser adapter, 2026-09-21. SPDX-License-Identifier: AGPL-3.0-or-later2+// The included extraction retains darktable's and LSD's original notices.3+#include "extracted.inc"4+static dt_iop_ashift_gui_data_t state;5+static dt_iop_module_t module = { &state };6+static float result[13]; // angle, vertical shift, horizontal shift, shear, 3x3 forward matrix7+static float *flat_lines;8+9+int detect(const uint8_t *rgba, int w, int h) {10+ if(w<2 || h<2 || w>1200 || h>1200) return -1;11+ free(state.lines); state.lines=NULL; free(flat_lines); flat_lines=NULL;12+ memset(&state,0,sizeof(state));13+ state.lines_in_width=w; state.lines_in_height=h;14+ state.rotation_range=ROTATION_RANGE; state.lensshift_v_range=LENSSHIFT_RANGE;15+ state.lensshift_h_range=LENSSHIFT_RANGE; state.shear_range=SHEAR_RANGE;16+ float *pixels=malloc((size_t)w*h*4*sizeof(float));17+ if(!pixels) return -1;18+ for(size_t i=0;i<(size_t)w*h*4;i++) pixels[i]=rgba[i]/255.0f;19+ line_detect(pixels,w,h,0,0,1,&state.lines,&state.lines_count,20+ &state.vertical_count,&state.horizontal_count,&state.vertical_weight,&state.horizontal_weight,0,FALSE);21+ free(pixels);22+ srand(42); // Repeatable results for repeated previews of the same photo.23+ if(!_remove_outliers(&module)) return -1;24+ flat_lines=malloc(MAX(1,state.lines_count)*6*sizeof(float));25+ return flat_lines ? state.lines_count : -1;26+}27+28+float *lines(void) {29+ if(!flat_lines) return NULL;30+ for(int i=0;i<state.lines_count;i++) {31+ const dt_iop_ashift_line_t *l=&state.lines[i];32+ flat_lines[i*6]=l->p1[0]; flat_lines[i*6+1]=l->p1[1];33+ flat_lines[i*6+2]=l->p2[0]; flat_lines[i*6+3]=l->p2[1];34+ flat_lines[i*6+4]=l->type; flat_lines[i*6+5]=l->weight;35+ }36+ return flat_lines;37+}38+39+int fit(int mode) {40+ dt_iop_ashift_params_t p={0}; p.mode=ASHIFT_MODE_GENERIC;41+ int dir=mode==1?ASHIFT_FIT_VERTICALLY:mode==2?ASHIFT_FIT_HORIZONTALLY:42+ mode==3?ASHIFT_FIT_BOTH_SHEAR:ASHIFT_FIT_ROTATION_BOTH_LINES;43+ int status=nmsfit(&module,&p,dir);44+ if(status) return status;45+ if(!isfinite(p.rotation)||!isfinite(p.lensshift_v)||!isfinite(p.lensshift_h)||!isfinite(p.shear)) return NMS_INSANE;46+ result[0]=p.rotation; result[1]=p.lensshift_v; result[2]=p.lensshift_h; result[3]=p.shear;47+ return 0;48+}49+50+float *matrix(int w,int h,float angle,float vertical,float horizontal,float shear) {51+ result[0]=angle;result[1]=vertical;result[2]=horizontal;result[3]=shear;52+ _homography(result+4,angle,vertical,horizontal,shear,DEFAULT_F_LENGTH,0,1,w,h,ASHIFT_HOMOGRAPH_FORWARD);53+ return result;54+}55+float *parameters(void) { return result; }56+57+// Inverse-mapped bilinear browser renderer; independent of darktable's pixelpipe.58+int warp(const uint8_t *src,int w,int h,uint8_t *dst,int ow,int oh,const float *inv,59+ float left,float top,float cw,float ch) {60+ if(w<2||h<2||ow<1||oh<1) return 1;61+ for(int y=0;y<oh;y++) for(int x=0;x<ow;x++) {62+ const float u=left+(x+0.5f)*cw/ow, v=top+(y+0.5f)*ch/oh;63+ const float z=inv[6]*u+inv[7]*v+inv[8];64+ const float sx=(inv[0]*u+inv[1]*v+inv[2])/z, sy=(inv[3]*u+inv[4]*v+inv[5])/z;65+ uint8_t *o=dst+4*((size_t)y*ow+x);66+ if(!isfinite(sx)||!isfinite(sy)||sx<0||sy<0||sx>w-1||sy>h-1) {67+ o[0]=o[1]=o[2]=245; o[3]=255; continue;68+ }69+ int ix=MIN((int)sx,w-2), iy=MIN((int)sy,h-2);70+ float fx=sx-ix,fy=sy-iy;71+ const uint8_t *p=src+4*((size_t)iy*w+ix);72+ for(int c=0;c<3;c++) o[c]=(uint8_t)CLAMP((1-fy)*((1-fx)*p[c]+fx*p[4+c])+fy*((1-fx)*p[w*4+c]+fx*p[w*4+4+c])+0.5f,0,255);73+ o[3]=255;74+ }75+ return 0;76+}
A native/extracted.inc +1460 -0
@@ -0,0 +1,1460 @@
1+/*2+ This file is part of darktable,3+ Copyright (C) 2016-2026 darktable developers.4+5+ darktable is free software: you can redistribute it and/or modify6+ it under the terms of the GNU General Public License as published by7+ the Free Software Foundation, either version 3 of the License, or8+ (at your option) any later version.9+10+ darktable is distributed in the hope that it will be useful,11+ but WITHOUT ANY WARRANTY; without even the implied warranty of12+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13+ GNU General Public License for more details.14+15+ You should have received a copy of the GNU General Public License16+ along with darktable. If not, see <http://www.gnu.org/licenses/>.17+*/18+19+// Extracted and adapted for Upright, 2026-09-21. See NOTICE.md.20+21+#include "compat.h"22+23+#define ROTATION_RANGE 10 // allowed min/max default range for rotation parameter24+#define ROTATION_RANGE_SOFT 180 // allowed min/max range for rotation parameter with manual adjustment25+#define LENSSHIFT_RANGE 1.0 // allowed min/max default range for lensshift parameters26+#define LENSSHIFT_RANGE_SOFT 2.0 // allowed min/max range for lensshift parameters with manual adjustment27+#define SHEAR_RANGE 0.2 // allowed min/max range for shear parameter28+#define SHEAR_RANGE_SOFT 0.5 // allowed min/max range for shear parameter with manual adjustment29+#define MIN_LINE_LENGTH 5 // the minimum length of a line in pixels to be regarded as relevant30+#define MAX_TANGENTIAL_DEVIATION 30 // by how many degrees a line may deviate from the +/-180 and +/-90 to be regarded as relevant31+#define LSD_SCALE 0.99 // LSD: scaling factor for line detection32+#define LSD_SIGMA_SCALE 0.6 // LSD: sigma for Gaussian filter is computed as sigma = sigma_scale/scale33+#define LSD_QUANT 2.0 // LSD: bound to the quantization error on the gradient norm34+#define LSD_ANG_TH 22.5 // LSD: gradient angle tolerance in degrees35+#define LSD_LOG_EPS 0.0 // LSD: detection threshold: -log10(NFA) > log_eps36+#define LSD_DENSITY_TH 0.7 // LSD: minimal density of region points in rectangle37+#define LSD_N_BINS 1024 // LSD: number of bins in pseudo-ordering of gradient modulus38+#define LSD_GAMMA 0.45 // gamma correction to apply on raw images prior to line detection39+#define RANSAC_RUNS 400 // how many iterations to run in ransac40+#define RANSAC_EPSILON 2 // starting value for ransac epsilon (in -log10 units)41+#define RANSAC_EPSILON_STEP 1 // step size of epsilon optimization (log10 units)42+#define RANSAC_ELIMINATION_RATIO 60 // percentage of lines we try to eliminate as outliers43+#define RANSAC_OPTIMIZATION_STEPS 5 // home many steps to optimize epsilon44+#define RANSAC_OPTIMIZATION_DRY_RUNS 50 // how man runs per optimization steps45+#define RANSAC_HURDLE 5 // hurdle rate: the number of lines below which we do a complete permutation instead of random sampling46+#define MINIMUM_FITLINES 2 // minimum number of lines needed for automatic parameter fit47+#define NMS_EPSILON 1e-3 // break criterion for Nelder-Mead simplex48+#define NMS_SCALE 1.0 // scaling factor for Nelder-Mead simplex49+#define NMS_ITERATIONS 400 // number of iterations for Nelder-Mead simplex50+#define NMS_CROP_EPSILON 100.0 // break criterion for Nelder-Mead simplex on crop fitting51+#define NMS_CROP_SCALE 0.5 // scaling factor for Nelder-Mead simplex on crop fitting52+#define NMS_CROP_ITERATIONS 100 // number of iterations for Nelder-Mead simplex on crop fitting53+#define NMS_ALPHA 1.0 // reflection coefficient for Nelder-Mead simplex54+#define NMS_BETA 0.5 // contraction coefficient for Nelder-Mead simplex55+#define NMS_GAMMA 2.0 // expansion coefficient for Nelder-Mead simplex56+#define DEFAULT_F_LENGTH 28.0 // focal length we assume if no exif data are available57+58+// define to get debugging output59+#undef ASHIFT_DEBUG60+61+// maximum number of drawn lines that can be saved in parameters62+// any change in this value needs to upgrade parameters version !63+#define MAX_SAVED_LINES 5064+65+66+#include "lsd.inc"67+#include "upstream/ashift_nmsimplex.c"68+69+typedef enum dt_iop_ashift_method_t70+{71+ ASHIFT_METHOD_NONE = 0,72+ ASHIFT_METHOD_AUTO,73+ ASHIFT_METHOD_QUAD,74+ ASHIFT_METHOD_LINES75+} dt_iop_ashift_method_t;76+77+typedef enum dt_iop_ashift_homodir_t78+{79+ ASHIFT_HOMOGRAPH_FORWARD,80+ ASHIFT_HOMOGRAPH_INVERTED81+} dt_iop_ashift_homodir_t;82+83+typedef enum dt_iop_ashift_linetype_t84+{85+ ASHIFT_LINE_IRRELEVANT = 0, // the line is found to be not interesting86+ // eg. too short, or not horizontal or vertical87+ ASHIFT_LINE_RELEVANT = 1 << 0, // the line is relevant for us88+ ASHIFT_LINE_DIRVERT = 1 << 1, // the line is (mostly) vertical, else (mostly) horizontal89+ ASHIFT_LINE_SELECTED = 1 << 2, // the line is selected for fitting90+ ASHIFT_LINE_VERTICAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT,91+ ASHIFT_LINE_HORIZONTAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT,92+ ASHIFT_LINE_VERTICAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED,93+ ASHIFT_LINE_HORIZONTAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED,94+ ASHIFT_LINE_MASK = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED95+} dt_iop_ashift_linetype_t;96+97+typedef enum dt_iop_ashift_linecolor_t98+{99+ ASHIFT_LINECOLOR_GREY = 0,100+ ASHIFT_LINECOLOR_GREEN = 1,101+ ASHIFT_LINECOLOR_RED = 2,102+ ASHIFT_LINECOLOR_BLUE = 3,103+ ASHIFT_LINECOLOR_YELLOW = 4104+} dt_iop_ashift_linecolor_t;105+106+typedef enum dt_iop_ashift_fitaxis_t107+{108+ ASHIFT_FIT_NONE = 0, // none109+ ASHIFT_FIT_ROTATION = 1 << 0, // flag indicates to fit rotation angle110+ ASHIFT_FIT_LENS_VERT = 1 << 1, // flag indicates to fit vertical lens shift111+ ASHIFT_FIT_LENS_HOR = 1 << 2, // flag indicates to fit horizontal lens shift112+ ASHIFT_FIT_SHEAR = 1 << 3, // flag indicates to fit shear parameter113+ ASHIFT_FIT_LINES_VERT = 1 << 4, // use vertical lines for fitting114+ ASHIFT_FIT_LINES_HOR = 1 << 5, // use horizontal lines for fitting115+ ASHIFT_FIT_LENS_BOTH = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR,116+ ASHIFT_FIT_LINES_BOTH = ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,117+ ASHIFT_FIT_VERTICALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT,118+ ASHIFT_FIT_HORIZONTALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR,119+ ASHIFT_FIT_BOTH = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |120+ ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,121+ ASHIFT_FIT_VERTICALLY_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT,122+ ASHIFT_FIT_HORIZONTALLY_NO_ROTATION = ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR,123+ ASHIFT_FIT_BOTH_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |124+ ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,125+ ASHIFT_FIT_BOTH_SHEAR = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |126+ ASHIFT_FIT_SHEAR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,127+ ASHIFT_FIT_ROTATION_VERTICAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT,128+ ASHIFT_FIT_ROTATION_HORIZONTAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_HOR,129+ ASHIFT_FIT_ROTATION_BOTH_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,130+ ASHIFT_FIT_FLIP = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR131+} dt_iop_ashift_fitaxis_t;132+133+typedef enum dt_iop_ashift_nmsresult_t134+{135+ NMS_SUCCESS = 0,136+ NMS_NOT_ENOUGH_LINES = 1,137+ NMS_DID_NOT_CONVERGE = 2,138+ NMS_INSANE = 3139+} dt_iop_ashift_nmsresult_t;140+141+typedef enum dt_iop_ashift_enhance_t142+{143+ ASHIFT_ENHANCE_NONE = 0,144+ ASHIFT_ENHANCE_EDGES = 1 << 0,145+ ASHIFT_ENHANCE_DETAIL = 1 << 1,146+ ASHIFT_ENHANCE_HORIZONTAL = 0x100,147+ ASHIFT_ENHANCE_VERTICAL = 0x200148+} dt_iop_ashift_enhance_t;149+150+typedef enum dt_iop_ashift_mode_t151+{152+ ASHIFT_MODE_GENERIC = 0, // $DESCRIPTION: "generic"153+ ASHIFT_MODE_SPECIFIC = 1 // $DESCRIPTION: "specific"154+} dt_iop_ashift_mode_t;155+156+typedef enum dt_iop_ashift_crop_t157+{158+ ASHIFT_CROP_OFF = 0, // $DESCRIPTION: "off"159+ ASHIFT_CROP_LARGEST = 1,// $DESCRIPTION: "largest area"160+ ASHIFT_CROP_ASPECT = 2 // $DESCRIPTION: "original format"161+} dt_iop_ashift_crop_t;162+163+typedef enum dt_iop_ashift_bounding_t164+{165+ ASHIFT_BOUNDING_OFF = 0,166+ ASHIFT_BOUNDING_SELECT = 1,167+ ASHIFT_BOUNDING_DESELECT = 2168+} dt_iop_ashift_bounding_t;169+170+typedef enum dt_iop_ashift_jobcode_t171+{172+ ASHIFT_JOBCODE_NONE = 0,173+ ASHIFT_JOBCODE_GET_STRUCTURE = 1,174+ ASHIFT_JOBCODE_FIT = 2,175+ ASHIFT_JOBCODE_GET_STRUCTURE_LINES = 3,176+ ASHIFT_JOBCODE_GET_STRUCTURE_QUAD = 4,177+ ASHIFT_JOBCODE_DO_CROP = 5178+} dt_iop_ashift_jobcode_t;179+180+typedef struct dt_iop_ashift_params_t181+{182+ float rotation; // $MIN: -ROTATION_RANGE_SOFT $MAX: ROTATION_RANGE_SOFT $DEFAULT: 0.0183+ float lensshift_v; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (vertical)"184+ float lensshift_h; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (horizontal)"185+ float shear; // $MIN: -SHEAR_RANGE_SOFT $MAX: SHEAR_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "shear"186+ float f_length; // $MIN: 1.0 $MAX: 2000.0 $DEFAULT: DEFAULT_F_LENGTH $DESCRIPTION: "focal length"187+ float crop_factor; // $MIN: 0.5 $MAX: 10.0 $DEFAULT: 1.0 $DESCRIPTION: "crop factor"188+ float orthocorr; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "lens dependence"189+ float aspect; // $MIN: 0.5 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "aspect adjust"190+ dt_iop_ashift_mode_t mode; // $DEFAULT: ASHIFT_MODE_GENERIC $DESCRIPTION: "lens model"191+ dt_iop_ashift_crop_t cropmode; // $DEFAULT: ASHIFT_CROP_LARGEST $DESCRIPTION: "automatic cropping"192+ float cl; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0193+ float cr; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0194+ float ct; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0195+ float cb; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0196+ float last_drawn_lines[MAX_SAVED_LINES * 4];197+ int last_drawn_lines_count;198+ float last_quad_lines[8];199+} dt_iop_ashift_params_t;200+201+typedef struct dt_iop_ashift_line_t202+{203+ float p1[3];204+ float p2[3];205+ float length;206+ float width;207+ float weight;208+ dt_iop_ashift_linetype_t type;209+ // homogeneous coordinates:210+ float DT_ALIGNED_PIXEL L[3];211+} dt_iop_ashift_line_t;212+213+typedef struct dt_iop_ashift_points_idx_t214+{215+ size_t offset;216+ int length;217+ int near;218+ int bounded;219+ dt_iop_ashift_linetype_t type;220+ dt_iop_ashift_linecolor_t color;221+ // bounding box:222+ float bbx, bby, bbX, bbY;223+} dt_iop_ashift_points_idx_t;224+225+typedef struct dt_iop_ashift_fit_params_t226+{227+ int params_count;228+ dt_iop_ashift_linetype_t linetype;229+ dt_iop_ashift_linetype_t linemask;230+ dt_iop_ashift_line_t *lines;231+ int lines_count;232+ int width;233+ int height;234+ float weight;235+ float f_length_kb;236+ float orthocorr;237+ float aspect;238+ float rotation;239+ float lensshift_v;240+ float lensshift_h;241+ float shear;242+ float rotation_range;243+ float lensshift_v_range;244+ float lensshift_h_range;245+ float shear_range;246+} dt_iop_ashift_fit_params_t;247+248+249+typedef struct {250+ dt_iop_ashift_line_t *lines;251+ int lines_count, lines_in_width, lines_in_height, lines_x_off, lines_y_off;252+ int vertical_count, horizontal_count, lines_version, isflipped;253+ float vertical_weight, horizontal_weight;254+ float rotation_range, lensshift_v_range, lensshift_h_range, shear_range;255+} dt_iop_ashift_gui_data_t;256+typedef struct { dt_iop_ashift_gui_data_t *gui_data; } dt_iop_module_t;257+258+// normalized product of two 3x1 vectors259+// dst needs to be different from v1 and v2260+static inline void vec3prodn(float *dst, const float *const v1, const float *const v2)261+{262+ const float l1 = v1[1] * v2[2] - v1[2] * v2[1];263+ const float l2 = v1[2] * v2[0] - v1[0] * v2[2];264+ const float l3 = v1[0] * v2[1] - v1[1] * v2[0];265+266+ // normalize so that l1^2 + l2^2 + l3^3 = 1267+ const float sq = sqrtf(l1 * l1 + l2 * l2 + l3 * l3);268+269+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;270+271+ dst[0] = l1 * f;272+ dst[1] = l2 * f;273+ dst[2] = l3 * f;274+}275+276+// normalize a 3x1 vector so that x^2 + y^2 + z^2 = 1277+// dst and v may be the same278+static inline void vec3norm(float *dst, const float *const v)279+{280+ const float sq = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);281+282+ // special handling for an all-zero vector283+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;284+285+ dst[0] = v[0] * f;286+ dst[1] = v[1] * f;287+ dst[2] = v[2] * f;288+}289+290+// normalize a 3x1 vector so that x^2 + y^2 = 1; a useful normalization for291+// lines in homogeneous coordinates292+// dst and v may be the same293+static inline void vec3lnorm(float *dst, const float *const v)294+{295+ const float sq = dt_fast_hypotf(v[0], v[1]);296+297+ // special handling for a point vector of the image center298+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;299+300+ dst[0] = v[0] * f;301+ dst[1] = v[1] * f;302+ dst[2] = v[2] * f;303+}304+305+306+// scalar product of two 3x1 vectors307+static inline float vec3scalar(const float *const v1, const float *const v2)308+{309+ return (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]);310+}311+312+// check if 3x1 vector is (very close to) null313+static inline int vec3isnull(const float *const v)314+{315+ const float eps = 1e-10f;316+ return (fabsf(v[0]) < eps && fabsf(v[1]) < eps && fabsf(v[2]) < eps);317+}318+319+320+#define MAT3SWAP(a, b) { float (*tmp)[3] = (a); (a) = (b); (b) = tmp; }321+322+static void _homography(float *homograph,323+ const float angle,324+ const float shift_v,325+ const float shift_h,326+ const float shear,327+ const float f_length_kb,328+ const float orthocorr,329+ const float aspect,330+ const int width,331+ const int height,332+ const dt_iop_ashift_homodir_t dir)333+{334+ // calculate homograph that combines all translations, rotations335+ // and warping into one single matrix operation.336+ // this is heavily leaning on ShiftN where the homographic matrix expects337+ // input in (y : x : 1) format. in the darktable world we want to keep the338+ // (x : y : 1) convention. therefore we need to flip coordinates first and339+ // make sure that output is in correct format after corrections are applied.340+341+ const float u = width;342+ const float v = height;343+344+ const float phi = deg2radf(angle);345+ const float cosi = cosf(phi);346+ const float sini = sinf(phi);347+ const float ascale = sqrtf(aspect);348+349+ // most of this comes from ShiftN350+ const float f_global = f_length_kb;351+ const float horifac = 1.0f - orthocorr / 100.0f;352+ const float exppa_v = expf(shift_v);353+ const float fdb_v = f_global / (14.4f + (v / u - 1) * 7.2f);354+ const float rad_v = fdb_v * (exppa_v - 1.0f) / (exppa_v + 1.0f);355+ const float alpha_v = CLAMP(atanf(rad_v), -1.5f, 1.5f);356+ const float rt_v = sinf(0.5f * alpha_v);357+ const float r_v = fmaxf(0.1f, 2.0f * (horifac - 1.0f) * rt_v * rt_v + 1.0f);358+359+ const float vertifac = 1.0f - orthocorr / 100.0f;360+ const float exppa_h = expf(shift_h);361+ const float fdb_h = f_global / (14.4f + (u / v - 1) * 7.2f);362+ const float rad_h = fdb_h * (exppa_h - 1.0f) / (exppa_h + 1.0f);363+ const float alpha_h = CLAMP(atanf(rad_h), -1.5f, 1.5f);364+ const float rt_h = sinf(0.5f * alpha_h);365+ const float r_h = fmaxf(0.1f, 2.0f * (vertifac - 1.0f) * rt_h * rt_h + 1.0f);366+367+368+ // three intermediate buffers for matrix calculation ...369+ float DT_ALIGNED_ARRAY m1[3][3];370+ float DT_ALIGNED_ARRAY m2[3][3];371+ float DT_ALIGNED_ARRAY m3[3][3];372+373+ // ... and some pointers to handle them more intuitively374+ float (*mwork)[3] = m1;375+ float (*minput)[3] = m2;376+ float (*moutput)[3] = m3;377+378+ // Step 1: flip x and y coordinates (see above)379+ memset(minput, 0, sizeof(float) * 9);380+ minput[0][1] = 1.0f;381+ minput[1][0] = 1.0f;382+ minput[2][2] = 1.0f;383+384+385+ // Step 2: rotation of image around its center386+ memset(mwork, 0, sizeof(float) * 9);387+ mwork[0][0] = cosi;388+ mwork[0][1] = -sini;389+ mwork[1][0] = sini;390+ mwork[1][1] = cosi;391+ mwork[0][2] = -0.5f * v * cosi + 0.5f * u * sini + 0.5f * v;392+ mwork[1][2] = -0.5f * v * sini - 0.5f * u * cosi + 0.5f * u;393+ mwork[2][2] = 1.0f;394+395+ // multiply mwork * minput -> moutput396+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);397+398+399+ // Step 3: apply shearing400+ memset(mwork, 0, sizeof(float) * 9);401+ mwork[0][0] = 1.0f;402+ mwork[0][1] = shear;403+ mwork[1][1] = 1.0f;404+ mwork[1][0] = shear;405+ mwork[2][2] = 1.0f;406+407+ // moutput (of last calculation) -> minput408+ MAT3SWAP(minput, moutput);409+ // multiply mwork * minput -> moutput410+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);411+412+413+ // Step 4: apply vertical lens shift effect414+ memset(mwork, 0, sizeof(float) * 9);415+ mwork[0][0] = exppa_v;416+ mwork[1][0] = 0.5f * ((exppa_v - 1.0f) * u) / v;417+ mwork[1][1] = 2.0f * exppa_v / (exppa_v + 1.0f);418+ mwork[1][2] = -0.5f * ((exppa_v - 1.0f) * u) / (exppa_v + 1.0f);419+ mwork[2][0] = (exppa_v - 1.0f) / v;420+ mwork[2][2] = 1.0f;421+422+ // moutput (of last calculation) -> minput423+ MAT3SWAP(minput, moutput);424+ // multiply mwork * minput -> moutput425+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);426+427+428+ // Step 5: horizontal compression429+ memset(mwork, 0, sizeof(float) * 9);430+ mwork[0][0] = 1.0f;431+ mwork[1][1] = r_v;432+ mwork[1][2] = 0.5f * u * (1.0f - r_v);433+ mwork[2][2] = 1.0f;434+435+ // moutput (of last calculation) -> minput436+ MAT3SWAP(minput, moutput);437+ // multiply mwork * minput -> moutput438+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);439+440+441+ // Step 6: flip x and y back again442+ memset(mwork, 0, sizeof(float) * 9);443+ mwork[0][1] = 1.0f;444+ mwork[1][0] = 1.0f;445+ mwork[2][2] = 1.0f;446+447+ // moutput (of last calculation) -> minput448+ MAT3SWAP(minput, moutput);449+ // multiply mwork * minput -> moutput450+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);451+452+453+ // from here output vectors would be in (x : y : 1) format454+455+ // Step 7: now we can apply horizontal lens shift with the same matrix format as above456+ memset(mwork, 0, sizeof(float) * 9);457+ mwork[0][0] = exppa_h;458+ mwork[1][0] = 0.5f * ((exppa_h - 1.0f) * v) / u;459+ mwork[1][1] = 2.0f * exppa_h / (exppa_h + 1.0f);460+ mwork[1][2] = -0.5f * ((exppa_h - 1.0f) * v) / (exppa_h + 1.0f);461+ mwork[2][0] = (exppa_h - 1.0f) / u;462+ mwork[2][2] = 1.0f;463+464+ // moutput (of last calculation) -> minput465+ MAT3SWAP(minput, moutput);466+ // multiply mwork * minput -> moutput467+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);468+469+470+ // Step 8: vertical compression471+ memset(mwork, 0, sizeof(float) * 9);472+ mwork[0][0] = 1.0f;473+ mwork[1][1] = r_h;474+ mwork[1][2] = 0.5f * v * (1.0f - r_h);475+ mwork[2][2] = 1.0f;476+477+ // moutput (of last calculation) -> minput478+ MAT3SWAP(minput, moutput);479+ // multiply mwork * minput -> moutput480+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);481+482+483+ // Step 9: apply aspect ratio scaling484+ memset(mwork, 0, sizeof(float) * 9);485+ mwork[0][0] = 1.0f * ascale;486+ mwork[1][1] = 1.0f / ascale;487+ mwork[2][2] = 1.0f;488+489+ // moutput (of last calculation) -> minput490+ MAT3SWAP(minput, moutput);491+ // multiply mwork * minput -> moutput492+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);493+494+495+ // Step 10: find x/y offsets and apply according correction so that496+ // no negative coordinates occur in output vector497+ float umin = FLT_MAX, vmin = FLT_MAX;498+ // visit all four corners499+ for(int y = 0; y < height; y += height - 1)500+ for(int x = 0; x < width; x += width - 1)501+ {502+ float pi[3], po[3];503+ pi[0] = x;504+ pi[1] = y;505+ pi[2] = 1.0f;506+ // moutput expects input in (x:y:1) format and gives output as (x:y:1)507+ mat3mulv(po, (float *)moutput, pi);508+ umin = MIN(umin, po[0] / po[2]);509+ vmin = MIN(vmin, po[1] / po[2]);510+ }511+512+ memset(mwork, 0, sizeof(float) * 9);513+ mwork[0][0] = 1.0f;514+ mwork[1][1] = 1.0f;515+ mwork[2][2] = 1.0f;516+ mwork[0][2] = -umin;517+ mwork[1][2] = -vmin;518+519+ // moutput (of last calculation) -> minput520+ MAT3SWAP(minput, moutput);521+ // multiply mwork * minput -> moutput522+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);523+524+525+ // on request we either keep the final matrix for forward conversions526+ // or produce an inverted matrix for backward conversions527+ if(dir == ASHIFT_HOMOGRAPH_FORWARD)528+ {529+ // we have what we need -> copy it to the right place530+ memcpy(homograph, moutput, sizeof(float) * 9);531+ }532+ else533+ {534+ // generate inverted homograph (mat3inv function defined in colorspaces.c)535+ if(mat3inv((float *)homograph, (float *)moutput))536+ {537+ // in case of error we set to unity matrix538+ memset(mwork, 0, sizeof(float) * 9);539+ mwork[0][0] = 1.0f;540+ mwork[1][1] = 1.0f;541+ mwork[2][2] = 1.0f;542+ memcpy(homograph, mwork, sizeof(float) * 9);543+ }544+ }545+}546+#undef MAT3SWAP547+548+549+550+static void rgb2grey256(const float *const in,551+ double *const out,552+ const int width,553+ const int height)554+{555+ const size_t npixels = (size_t)width * height;556+557+ DT_OMP_FOR()558+ for(size_t index = 0; index < npixels; index++)559+ {560+ out[index] = (0.3f * in[4*index+0]561+ + 0.59f * in[4*index+1]562+ + 0.11f * in[4*index+2]) * 256.0;563+ }564+}565+566+567+static gboolean line_detect(float *in,568+ const int width,569+ const int height,570+ const int x_off,571+ const int y_off,572+ const float scale,573+ dt_iop_ashift_line_t **alines,574+ int *lcount,575+ int *vcount,576+ int *hcount,577+ float *vweight,578+ float *hweight,579+ const dt_iop_ashift_enhance_t enhance,580+ const gboolean is_raw)581+{582+ double *greyscale = NULL;583+ double *lsd_lines = NULL;584+ dt_iop_ashift_line_t *ashift_lines = NULL;585+586+ int vertical_count = 0;587+ int horizontal_count = 0;588+ float vertical_weight = 0.0f;589+ float horizontal_weight = 0.0f;590+591+ // Browser input is already display-referred; no RAW/detail preprocessing.592+ // allocate intermediate buffers593+ greyscale = malloc(sizeof(double) * width * height);594+ if(greyscale == NULL) goto error;595+596+ // convert to greyscale image597+ rgb2grey256(in, greyscale, width, height);598+599+ // call the line segment detector LSD;600+ // LSD stores the number of found lines in lines_count.601+ // it returns structural details as vector 'double lines[7 * lines_count]'602+ int lines_count;603+604+ lsd_lines = LineSegmentDetection(&lines_count, greyscale, width, height,605+ LSD_SCALE, LSD_SIGMA_SCALE, LSD_QUANT,606+ LSD_ANG_TH, LSD_LOG_EPS, LSD_DENSITY_TH,607+ LSD_N_BINS, NULL, NULL, NULL);608+609+ // we count the lines that we really want to use610+ int lct = 0;611+ if(lines_count > 0)612+ {613+ // aggregate lines data into our own structures614+ ashift_lines = malloc(sizeof(dt_iop_ashift_line_t) * lines_count);615+616+ if(ashift_lines == NULL) goto error;617+618+ for(int n = 0; n < lines_count; n++)619+ {620+ const float x1 = lsd_lines[n * 7 + 0];621+ const float y1 = lsd_lines[n * 7 + 1];622+ const float x2 = lsd_lines[n * 7 + 2];623+ const float y2 = lsd_lines[n * 7 + 3];624+625+ // check for lines running along image borders and skip them.626+ // these would likely be false-positives which could result627+ // from any kind of processing artifacts628+ if((fabsf(x1 - x2) < 1 && fmaxf(x1, x2) < 2)629+ || (fabsf(x1 - x2) < 1 && fminf(x1, x2) > width - 3)630+ || (fabsf(y1 - y2) < 1 && fmaxf(y1, y2) < 2)631+ || (fabsf(y1 - y2) < 1 && fminf(y1, y2) > height - 3))632+ continue;633+634+ // line position in absolute coordinates635+ float px1 = x_off + x1;636+ float py1 = y_off + y1;637+ float px2 = x_off + x2;638+ float py2 = y_off + y2;639+640+ // scale back to input buffer641+ px1 /= scale;642+ py1 /= scale;643+ px2 /= scale;644+ py2 /= scale;645+646+ // store as homogeneous coordinates647+ ashift_lines[lct].p1[0] = px1;648+ ashift_lines[lct].p1[1] = py1;649+ ashift_lines[lct].p1[2] = 1.0f;650+ ashift_lines[lct].p2[0] = px2;651+ ashift_lines[lct].p2[1] = py2;652+ ashift_lines[lct].p2[2] = 1.0f;653+654+ // calculate homogeneous coordinates of connecting line (defined by the two points)655+ vec3prodn(ashift_lines[lct].L, ashift_lines[lct].p1, ashift_lines[lct].p2);656+657+ // normalaze line coordinates so that x^2 + y^2 = 1658+ // (this will always succeed as L is a real line connecting two real points)659+ vec3lnorm(ashift_lines[lct].L, ashift_lines[lct].L);660+661+ // length and width of rectangle (see LSD)662+ ashift_lines[lct].length = sqrt((px2 - px1) * (px2 - px1)663+ + (py2 - py1) * (py2 - py1));664+ ashift_lines[lct].width = lsd_lines[n * 7 + 4] / scale;665+666+ // ... and weight (= length * width * angle precision)667+ const float weight = ashift_lines[lct].length668+ * ashift_lines[lct].width669+ * lsd_lines[n * 7 + 5];670+671+ ashift_lines[lct].weight = weight;672+673+674+ const float angle = rad2degf(atan2f(py2 - py1, px2 - px1));675+ const gboolean vertical = fabsf(fabsf(angle) - 90.0f) < MAX_TANGENTIAL_DEVIATION;676+ const gboolean horizontal = fabsf(fabsf(fabsf(angle) - 90.0f) - 90.0f) < MAX_TANGENTIAL_DEVIATION;677+ const gboolean relevant = ashift_lines[lct].length > MIN_LINE_LENGTH;678+679+ // register type of line680+ dt_iop_ashift_linetype_t type = ASHIFT_LINE_IRRELEVANT;681+682+ if(vertical && relevant)683+ {684+ type = ASHIFT_LINE_VERTICAL_SELECTED;685+ vertical_count++;686+ vertical_weight += weight;687+ }688+ else if(horizontal && relevant)689+ {690+ type = ASHIFT_LINE_HORIZONTAL_SELECTED;691+ horizontal_count++;692+ horizontal_weight += weight;693+ }694+ ashift_lines[lct].type = type;695+696+ // the next valid line697+ lct++;698+ }699+ }700+#ifdef ASHIFT_DEBUG701+ printf("%d lines (vertical %d, horizontal %d, not relevant %d)\n",702+ lines_count, vertical_count,703+ horizontal_count, lct - vertical_count - horizontal_count);704+ float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN;705+ for(int n = 0; n < lct; n++)706+ {707+ xmin = fmin(xmin, fmin(ashift_lines[n].p1[0], ashift_lines[n].p2[0]));708+ xmax = fmax(xmax, fmax(ashift_lines[n].p1[0], ashift_lines[n].p2[0]));709+ ymin = fmin(ymin, fmin(ashift_lines[n].p1[1], ashift_lines[n].p2[1]));710+ ymax = fmax(ymax, fmax(ashift_lines[n].p1[1], ashift_lines[n].p2[1]));711+ printf("x1 %.0f, y1 %.0f, x2 %.0f, y2 %.0f,"712+ " length %.0f, width %f, X %f, Y %f, Z %f, type %d, scalars %f %f\n",713+ ashift_lines[n].p1[0], ashift_lines[n].p1[1], ashift_lines[n].p2[0],714+ ashift_lines[n].p2[1],715+ ashift_lines[n].length, ashift_lines[n].width,716+ ashift_lines[n].L[0], ashift_lines[n].L[1], ashift_lines[n].L[2],717+ ashift_lines[n].type,718+ vec3scalar(ashift_lines[n].p1, ashift_lines[n].L),719+ vec3scalar(ashift_lines[n].p2, ashift_lines[n].L));720+ }721+ printf("xmin %.0f, xmax %.0f, ymin %.0f, ymax %.0f\n", xmin, xmax, ymin, ymax);722+#endif723+724+ // store results in provided locations725+ *lcount = lct;726+ *vcount = vertical_count;727+ *vweight = vertical_weight;728+ *hcount = horizontal_count;729+ *hweight = horizontal_weight;730+ *alines = ashift_lines;731+732+ // free intermediate buffers733+ free(lsd_lines);734+ free(greyscale);735+ return lct > 0;736+737+error:738+ free(lsd_lines);739+ free(greyscale);740+ return FALSE;741+}742+743+744+static inline void swap(int *a, int *b)745+{746+ const int tmp = *a;747+ *a = *b;748+ *b = tmp;749+}750+751+// do complete permutations752+static gboolean quickperm(int *a, int *p, const int N, int *i)753+{754+ if(*i >= N) return FALSE;755+756+ p[*i]--;757+ const int j = (*i % 2 == 1) ? p[*i] : 0;758+ swap(&a[j], &a[*i]);759+ *i = 1;760+ while(p[*i] == 0)761+ {762+ p[*i] = *i;763+ (*i)++;764+ }765+ return TRUE;766+}767+768+// Fisher-Yates shuffle769+static void shuffle(int *a, const int N)770+{771+ for(int i = 0; i < N; i++)772+ {773+ const int j = i + rand() % (N - i);774+ swap(&a[j], &a[i]);775+ }776+}777+778+// factorial function779+static int fact(const int n)780+{781+ return (n == 1 ? 1 : n * fact(n - 1));782+}783+784+// We use a pseudo-RANSAC algorithm to elminiate ouliers from our set of lines. The785+// original RANSAC works on linear optimization problems. Our model is nonlinear. We786+// take advantage of the fact that lines interesting for our model are vantage lines787+// that meet in one vantage point for each subset of lines (vertical/horizontal).788+// Strategy: we construct a model by (random) sampling within the subset of lines and789+// calculate the vantage point. Then we check the "distance" of all other lines to the790+// vantage point. The model that gives highest number of lines combined with the highest791+// total weight and lowest overall "distance" wins.792+// Disadvantage: compared to the original RANSAC we don't get any model parameters that793+// we could use for the following NMS fit.794+//795+// Self-tuning: we optimize "epsilon", the hurdle rate to reject a796+// line as an outlier, by a number of dry runs first. The target797+// average percentage value of lines to eliminate as outliers (without798+// judging on the quality of the model) is given by799+// RANSAC_ELIMINATION_RATIO,800+//801+// note: the actual percentage of outliers removed in the final run802+// will be lower because we will finally look for the best quality803+// model with the optimized epsilon and that quality value also804+// encloses the number of good lines.805+806+static void ransac(const dt_iop_ashift_line_t *lines,807+ int *index_set,808+ int *inout_set,809+ const int set_count,810+ const float total_weight,811+ const int xmin,812+ const int xmax,813+ const int ymin,814+ const int ymax)815+{816+ if(set_count < 3) return;817+818+ const size_t set_size = set_count * sizeof(int);819+ int *best_set = malloc(set_size);820+ memcpy(best_set, index_set, set_size);821+ int *best_inout = calloc(1, set_size);822+823+ float best_quality = 0.0f;824+825+ // hurdle value epsilon for rejecting a line as an outlier will be self-tuning826+ // in a number of dry runs827+ float epsilon = powf(10.0f, -RANSAC_EPSILON);828+ float epsilon_step = RANSAC_EPSILON_STEP;829+ // some accounting variables for self-tuning830+ int lines_eliminated = 0;831+ int valid_runs = 0;832+833+ // number of runs to optimize epsilon834+ const int optiruns = RANSAC_OPTIMIZATION_STEPS * RANSAC_OPTIMIZATION_DRY_RUNS;835+ // go for complete permutations on small set sizes, else for random sample consensus836+ const int riter = (set_count > RANSAC_HURDLE) ? RANSAC_RUNS : fact(set_count);837+838+ // some data needed for quickperm839+ int *perm = malloc(sizeof(int) * (set_count + 1));840+ for(int n = 0; n < set_count + 1; n++) perm[n] = n;841+ int piter = 1;842+843+ // inout holds good/bad qualification for each line844+ int *inout = malloc(set_size);845+846+ for(int r = 0; r < optiruns + riter; r++)847+ {848+ // get random or systematic variation of index set849+ if(set_count > RANSAC_HURDLE || r < optiruns)850+ shuffle(index_set, set_count);851+ else852+ (void)quickperm(index_set, perm, set_count, &piter);853+854+ // summed quality evaluation of this run855+ float quality = 0.0f;856+857+ // we build a model ouf of the first two lines858+ const float *L1 = lines[index_set[0]].L;859+ const float *L2 = lines[index_set[1]].L;860+861+ // get intersection point (ideally a vantage point)862+ float DT_ALIGNED_PIXEL V[3];863+ vec3prodn(V, L1, L2);864+865+ // catch special cases:866+ // a) L1 and L2 are identical -> V is NULL -> no valid vantage point867+ // b) vantage point lies inside image frame (no chance to correct for this case)868+ if(vec3isnull(V) ||869+ (fabsf(V[2]) > 0.0f &&870+ V[0]/V[2] >= xmin &&871+ V[1]/V[2] >= ymin &&872+ V[0]/V[2] <= xmax &&873+ V[1]/V[2] <= ymax))874+ {875+ // no valid model876+ quality = 0.0f;877+ }878+ else879+ {880+ // valid model881+882+ // normalize V so that x^2 + y^2 + z^2 = 1883+ vec3norm(V, V);884+885+ // the two lines constituting the model are part of the set886+ inout[0] = 1;887+ inout[1] = 1;888+889+ // go through all remaining lines, check if they are within the model, and890+ // mark that fact in inout[].891+ // summarize a quality parameter for all lines within the model892+ for(int n = 2; n < set_count; n++)893+ {894+ // L is normalized so that x^2 + y^2 = 1895+ const float *L3 = lines[index_set[n]].L;896+897+ // we take the absolute value of the dot product of V and L as898+ // a measure of the "distance" between point and line. Note899+ // that this is not the real euclidean distance but - with the900+ // given normalization - just a pragmatically selected number901+ // that goes to zero if V lies on L and increases the more V902+ // and L are apart903+ const float d = fabsf(vec3scalar(V, L3));904+905+ // depending on d we either include or exclude the point from the set906+ inout[n] = (d < epsilon) ? 1 : 0;907+908+ float q;909+910+ if(inout[n] == 1)911+ {912+ // a quality parameter that depends 1/3 on the number of913+ // lines within the model, 1/3 on their weight, and 1/3 on914+ // their weighted distance d to the vantage point915+ q = 0.33f / (float)set_count916+ + 0.33f * lines[index_set[n]].weight / total_weight917+ + 0.33f * (1.0f - d / epsilon)918+ * (float)set_count * lines[index_set[n]].weight / total_weight;919+ }920+ else921+ {922+ q = 0.0f;923+ lines_eliminated++;924+ }925+926+ quality += q;927+ }928+ valid_runs++;929+ }930+931+ if(r < optiruns)932+ {933+ // on last run of each self-tuning step934+ if((r % RANSAC_OPTIMIZATION_DRY_RUNS) == (RANSAC_OPTIMIZATION_DRY_RUNS - 1)935+ && (valid_runs > 0))936+ {937+#ifdef ASHIFT_DEBUG938+ printf("ransac self-tuning (run %d): epsilon %f", r, epsilon);939+#endif940+ // average ratio of lines that we eliminated with the given epsilon941+ float ratio = 100.0f * (float)lines_eliminated / ((float)set_count * valid_runs);942+ // adjust epsilon accordingly943+ if(ratio < RANSAC_ELIMINATION_RATIO)944+ epsilon = powf(10.0f, log10(epsilon) - epsilon_step);945+ else if(ratio > RANSAC_ELIMINATION_RATIO)946+ epsilon = powf(10.0f, log10(epsilon) + epsilon_step);947+#ifdef ASHIFT_DEBUG948+ printf(" (elimination ratio %f) -> %f\n", ratio, epsilon);949+#endif950+ // reduce step-size for next optimization round951+ epsilon_step /= 2.0f;952+ lines_eliminated = 0;953+ valid_runs = 0;954+ }955+ }956+ else957+ {958+ // in the "real" runs check against the best model found so far959+ if(quality > best_quality)960+ {961+ memcpy(best_set, index_set, set_size);962+ memcpy(best_inout, inout, set_size);963+ best_quality = quality;964+ }965+ }966+967+#ifdef ASHIFT_DEBUG968+ // report some statistics969+ int count = 0, lastcount = 0;970+ for(int n = 0; n < set_count; n++) count += best_inout[n];971+ for(int n = 0; n < set_count; n++) lastcount += inout[n];972+ printf("ransac run %d: best qual %.6f, eps %.6f,"973+ " line count %d of %d (this run: qual %.5f, count %d (%2f%%))\n", r,974+ best_quality, epsilon, count, set_count, quality, lastcount,975+ 100.0f * lastcount / (float)set_count);976+#endif977+ }978+979+ // store back best set980+ memcpy(index_set, best_set, set_size);981+ memcpy(inout_set, best_inout, set_size);982+983+ free(inout);984+ free(perm);985+ free(best_inout);986+ free(best_set);987+}988+989+990+// try to clean up structural data by eliminating outliers and thereby increasing991+// the chance of a convergent fitting992+static gboolean _remove_outliers(const dt_iop_module_t *self)993+{994+ dt_iop_ashift_gui_data_t *g = self->gui_data;995+996+ const int width = g->lines_in_width;997+ const int height = g->lines_in_height;998+ const int xmin = g->lines_x_off;999+ const int ymin = g->lines_y_off;1000+ const int xmax = xmin + width;1001+ const int ymax = ymin + height;1002+1003+ if(g->lines_count < 1)1004+ return TRUE;1005+ // holds the index set of lines we want to work on1006+ int *lines_set = malloc(sizeof(int) * g->lines_count);1007+ // holds the result of ransac1008+ int *inout_set = malloc(sizeof(int) * g->lines_count);1009+1010+ if(!lines_set || !inout_set)1011+ goto error;1012+ // some accounting variables1013+ int vnb = 0, vcount = 0;1014+ int hnb = 0, hcount = 0;1015+1016+ // just to be on the safe side1017+ if(g->lines == NULL) goto error;1018+1019+ // generate index list for the vertical lines1020+ for(int n = 0; n < g->lines_count; n++)1021+ {1022+ // is this a selected vertical line?1023+ if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_VERTICAL_SELECTED)1024+ continue;1025+1026+ lines_set[vnb] = n;1027+ inout_set[vnb] = 1;1028+ vnb++;1029+ }1030+1031+ // it only makes sense to call ransac if we have more than two lines1032+ if(vnb > 2)1033+ ransac(g->lines, lines_set, inout_set, vnb, g->vertical_weight,1034+ xmin, xmax, ymin, ymax);1035+1036+ // adjust line selected flag according to the ransac results1037+ for(int n = 0; n < vnb; n++)1038+ {1039+ const int m = lines_set[n];1040+ if(inout_set[n] == 1)1041+ {1042+ g->lines[m].type |= ASHIFT_LINE_SELECTED;1043+ vcount++;1044+ }1045+ else1046+ g->lines[m].type &= ~ASHIFT_LINE_SELECTED;1047+ }1048+ // update number of vertical lines1049+ g->vertical_count = vcount;1050+ g->lines_version++;1051+1052+ // now generate index list for the horizontal lines1053+ for(int n = 0; n < g->lines_count; n++)1054+ {1055+ // is this a selected horizontal line?1056+ if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_HORIZONTAL_SELECTED)1057+ continue;1058+1059+ lines_set[hnb] = n;1060+ inout_set[hnb] = 1;1061+ hnb++;1062+ }1063+1064+ // it only makes sense to call ransac if we have more than two lines1065+ if(hnb > 2)1066+ ransac(g->lines, lines_set, inout_set, hnb, g->horizontal_weight,1067+ xmin, xmax, ymin, ymax);1068+1069+ // adjust line selected flag according to the ransac results1070+ for(int n = 0; n < hnb; n++)1071+ {1072+ const int m = lines_set[n];1073+ if(inout_set[n] == 1)1074+ {1075+ g->lines[m].type |= ASHIFT_LINE_SELECTED;1076+ hcount++;1077+ }1078+ else1079+ g->lines[m].type &= ~ASHIFT_LINE_SELECTED;1080+ }1081+ // update number of horizontal lines1082+ g->horizontal_count = hcount;1083+ g->lines_version++;1084+1085+ free(inout_set);1086+ free(lines_set);1087+1088+ return TRUE;1089+1090+error:1091+ free(inout_set);1092+ free(lines_set);1093+ return FALSE;1094+}1095+1096+// utility function to map a variable in [min; max] to [-INF; + INF]1097+static inline double logit(const double x, const double min, const double max)1098+{1099+ const double eps = 1.0e-6;1100+ // make sure p does not touch the borders of its definition area,1101+ // not critical for data accuracy as logit() is only used on initial fit parameters1102+ const double p = CLAMP((x - min) / (max - min), eps, 1.0 - eps);1103+1104+ return (2.0 * atanh(2.0 * p - 1.0));1105+}1106+1107+// inverted function to logit()1108+static inline double ilogit(const double L, const double min, const double max)1109+{1110+ const double p = 0.5 * (1.0 + tanh(0.5 * L));1111+1112+ return (p * (max - min) + min);1113+}1114+1115+// helper function for simplex() return quality parameter for the given model1116+// strategy:1117+// * generate homography matrix out of fixed parameters and fitting parameters1118+// * apply homography to all end points of affected lines1119+// * generate new line out of transformed end points1120+// * calculate scalar product s of line with perpendicular axis1121+// * sum over weighted s^2 values1122+1123+static double model_fitness(double *params, void *data)1124+{1125+ const dt_iop_ashift_fit_params_t *fit = (dt_iop_ashift_fit_params_t *)data;1126+1127+ // just for convenience: get shorter names1128+ const dt_iop_ashift_line_t *lines = fit->lines;1129+ const int lines_count = fit->lines_count;1130+ const int width = fit->width;1131+ const int height = fit->height;1132+ const float f_length_kb = fit->f_length_kb;1133+ const float orthocorr = fit->orthocorr;1134+ const float aspect = fit->aspect;1135+1136+ float rotation = fit->rotation;1137+ float lensshift_v = fit->lensshift_v;1138+ float lensshift_h = fit->lensshift_h;1139+ float shear = fit->shear;1140+ const float rotation_range = fit->rotation_range;1141+ const float lensshift_v_range = fit->lensshift_v_range;1142+ const float lensshift_h_range = fit->lensshift_h_range;1143+ const float shear_range = fit->shear_range;1144+1145+ int pcount = 0;1146+1147+ // fill in fit parameters from params[]. Attention: order matters!!!1148+ if(dt_isnan(rotation))1149+ {1150+ rotation = ilogit(params[pcount], -rotation_range, rotation_range);1151+ pcount++;1152+ }1153+1154+ if(dt_isnan(lensshift_v))1155+ {1156+ lensshift_v = ilogit(params[pcount], -lensshift_v_range, lensshift_v_range);1157+ pcount++;1158+ }1159+1160+ if(dt_isnan(lensshift_h))1161+ {1162+ lensshift_h = ilogit(params[pcount], -lensshift_h_range, lensshift_h_range);1163+ pcount++;1164+ }1165+1166+ if(dt_isnan(shear))1167+ {1168+ shear = ilogit(params[pcount], -shear_range, shear_range);1169+ pcount++;1170+ }1171+1172+ assert(pcount == fit->params_count);1173+1174+ // the possible reference axes1175+ const float DT_ALIGNED_PIXEL Av[3] = { 1.0f, 0.0f, 0.0f };1176+ const float DT_ALIGNED_PIXEL Ah[3] = { 0.0f, 1.0f, 0.0f };1177+1178+ // generate homograph out of the parameters1179+ float DT_ALIGNED_ARRAY homograph[3][3];1180+ _homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, f_length_kb,1181+ orthocorr, aspect, width, height, ASHIFT_HOMOGRAPH_FORWARD);1182+1183+ // accounting variables1184+ double sumsq_v = 0.0;1185+ double sumsq_h = 0.0;1186+ double weight_v = 0.0;1187+ double weight_h = 0.0;1188+ int count_v = 0;1189+ int count_h = 0;1190+ int count = 0;1191+1192+ // iterate over all lines1193+ for(int n = 0; n < lines_count; n++)1194+ {1195+ // check if this is a line which we must skip1196+ if((lines[n].type & fit->linemask) != fit->linetype)1197+ continue;1198+1199+ // the direction of this line (vertical?)1200+ const int isvertical = lines[n].type & ASHIFT_LINE_DIRVERT;1201+1202+ // select the perpendicular reference axis1203+ const float *A = isvertical ? Ah : Av;1204+1205+ // apply homographic transformation to the end points1206+ float DT_ALIGNED_PIXEL P1[3];1207+ float DT_ALIGNED_PIXEL P2[3];1208+ mat3mulv(P1, (float *)homograph, lines[n].p1);1209+ mat3mulv(P2, (float *)homograph, lines[n].p2);1210+1211+ // get line connecting the two points1212+ float DT_ALIGNED_PIXEL L[3];1213+ vec3prodn(L, P1, P2);1214+1215+ // normalize L so that x^2 + y^2 = 1; makes sure that1216+ // y^2 = 1 / (1 + m^2) and x^2 = m^2 / (1 + m^2) with m defining the slope of the line1217+ vec3lnorm(L, L);1218+1219+ // get scalar product of line L with orthogonal axis A -> gives 01220+ // if line is perpendicular1221+ const float s = vec3scalar(L, A);1222+1223+ // sum up weighted s^2 for both directions individually1224+ sumsq_v += isvertical ? s * s * lines[n].weight : 0.0;1225+ weight_v += isvertical ? lines[n].weight : 0.0;1226+ count_v += isvertical ? 1 : 0;1227+ sumsq_h += !isvertical ? s * s * lines[n].weight : 0.0;1228+ weight_h += !isvertical ? lines[n].weight : 0.0;1229+ count_h += !isvertical ? 1 : 0;1230+ count++;1231+ }1232+1233+ const double v = weight_v > 0.0f && count > 01234+ ? sumsq_v / weight_v * (float)count_v / count1235+ : 0.0;1236+1237+ const double h = weight_h > 0.0f && count > 01238+ ? sumsq_h / weight_h * (float)count_h / count1239+ : 0.0;1240+1241+ double sum = sqrt(1.0 - (1.0 - v) * (1.0 - h)) * 1.0e6;1242+ //double sum = sqrt(v + h) * 1.0e6;1243+1244+#ifdef ASHIFT_DEBUG1245+ printf("fitness with rotation %f, lensshift_v %f,"1246+ " lensshift_h %f, shear %f -> lines %d, quality %10f\n",1247+ rotation, lensshift_v, lensshift_h, shear, count, sum);1248+#endif1249+1250+ return sum;1251+}1252+1253+// setup all data structures for fitting and call NM simplex1254+static dt_iop_ashift_nmsresult_t nmsfit(dt_iop_module_t *self,1255+ dt_iop_ashift_params_t *p,1256+ const dt_iop_ashift_fitaxis_t dir)1257+{1258+ const dt_iop_ashift_gui_data_t *g = self->gui_data;1259+1260+ if(!g->lines) return NMS_NOT_ENOUGH_LINES;1261+ if(dir == ASHIFT_FIT_NONE) return NMS_SUCCESS;1262+1263+ double params[4];1264+ int pcount = 0;1265+ int enough_lines = TRUE;1266+1267+ // initialize fit parameters1268+ dt_iop_ashift_fit_params_t fit;1269+ fit.lines = g->lines;1270+ fit.lines_count = g->lines_count;1271+ fit.width = g->lines_in_width;1272+ fit.height = g->lines_in_height;1273+ fit.f_length_kb = (p->mode == ASHIFT_MODE_GENERIC)1274+ ? DEFAULT_F_LENGTH1275+ : p->f_length * p->crop_factor;1276+ fit.orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr;1277+ fit.aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect;1278+ fit.rotation = p->rotation;1279+ fit.lensshift_v = p->lensshift_v;1280+ fit.lensshift_h = p->lensshift_h;1281+ fit.shear = p->shear;1282+ fit.rotation_range = g->rotation_range;1283+ fit.lensshift_v_range = g->lensshift_v_range;1284+ fit.lensshift_h_range = g->lensshift_h_range;1285+ fit.shear_range = g->shear_range;1286+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;1287+ fit.linemask = ASHIFT_LINE_MASK;1288+ fit.params_count = 0;1289+ fit.weight = 0.0f;1290+1291+ // if the image is flipped and if we do not want to fit both lens shift1292+ // directions or none at all, then we need to change direction1293+ dt_iop_ashift_fitaxis_t mdir = dir;1294+ if((mdir & ASHIFT_FIT_LENS_BOTH) != ASHIFT_FIT_LENS_BOTH &&1295+ (mdir & ASHIFT_FIT_LENS_BOTH) != 0)1296+ {1297+ // flip all directions1298+ mdir ^= g->isflipped ? ASHIFT_FIT_FLIP : 0;1299+ // special case that needs to be corrected1300+ mdir |= (mdir & ASHIFT_FIT_LINES_BOTH) == 0 ? ASHIFT_FIT_LINES_BOTH : 0;1301+ }1302+1303+1304+ // prepare fit structure and starting parameters for simplex fit.1305+ // note: the sequence of parameters in params[] needs to match the1306+ // respective order in dt_iop_ashift_fit_params_t. Parameters which are1307+ // to be fittet are marked with NAN in the fit structure. Non-NAN1308+ // parameters are assumed to be constant.1309+ if(mdir & ASHIFT_FIT_ROTATION)1310+ {1311+ // we fit rotation1312+ fit.params_count++;1313+ params[pcount] = logit(fit.rotation, -fit.rotation_range, fit.rotation_range);1314+ pcount++;1315+ fit.rotation = NAN;1316+ }1317+1318+ if(mdir & ASHIFT_FIT_LENS_VERT)1319+ {1320+ // we fit vertical lens shift1321+ fit.params_count++;1322+ params[pcount] = logit(fit.lensshift_v, -fit.lensshift_v_range, fit.lensshift_v_range);1323+ pcount++;1324+ fit.lensshift_v = NAN;1325+ }1326+1327+ if(mdir & ASHIFT_FIT_LENS_HOR)1328+ {1329+ // we fit horizontal lens shift1330+ fit.params_count++;1331+ params[pcount] = logit(fit.lensshift_h, -fit.lensshift_h_range, fit.lensshift_h_range);1332+ pcount++;1333+ fit.lensshift_h = NAN;1334+ }1335+1336+ if(mdir & ASHIFT_FIT_SHEAR)1337+ {1338+ // we fit the shear parameter1339+ fit.params_count++;1340+ params[pcount] = logit(fit.shear, -fit.shear_range, fit.shear_range);1341+ pcount++;1342+ fit.shear = NAN;1343+ }1344+1345+ if(mdir & ASHIFT_FIT_LINES_VERT)1346+ {1347+ // we use vertical lines for fitting1348+ fit.linetype |= ASHIFT_LINE_DIRVERT;1349+ fit.weight += g->vertical_weight;1350+ enough_lines = enough_lines && (g->vertical_count >= MINIMUM_FITLINES);1351+ }1352+1353+ if(mdir & ASHIFT_FIT_LINES_HOR)1354+ {1355+ // we use horizontal lines for fitting1356+ fit.linetype |= 0;1357+ fit.weight += g->horizontal_weight;1358+ enough_lines = enough_lines && (g->horizontal_count >= MINIMUM_FITLINES);1359+ }1360+1361+ // this needs to come after ASHIFT_FIT_LINES_VERT and ASHIFT_FIT_LINES_HOR1362+ if((mdir & ASHIFT_FIT_LINES_BOTH) == ASHIFT_FIT_LINES_BOTH)1363+ {1364+ // if we use fitting in both directions we need to1365+ // adjust fit.linetype and fit.linemask to match all selected lines1366+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;1367+ fit.linemask = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;1368+ }1369+1370+ // error case: we do not run simplex if there are not enough lines1371+ if(!enough_lines)1372+ {1373+#ifdef ASHIFT_DEBUG1374+ printf("optimization not possible: insufficient number of lines\n");1375+#endif1376+ return NMS_NOT_ENOUGH_LINES;1377+ }1378+1379+ // start the simplex fit1380+ const int iter = simplex(model_fitness, params, fit.params_count,1381+ NMS_EPSILON, NMS_SCALE, NMS_ITERATIONS, NULL, (void*)&fit);1382+1383+ // error case: the fit did not converge1384+ if(iter >= NMS_ITERATIONS)1385+ {1386+#ifdef ASHIFT_DEBUG1387+ printf("optimization not successful: maximum number of iterations reached (%d)\n", iter);1388+#endif1389+ return NMS_DID_NOT_CONVERGE;1390+ }1391+1392+ // fit was successful: now consolidate the results (order matters!!!)1393+ pcount = 0;1394+ fit.rotation = dt_isnan(fit.rotation)1395+ ? ilogit(params[pcount++], -fit.rotation_range, fit.rotation_range)1396+ : fit.rotation;1397+1398+ fit.lensshift_v = dt_isnan(fit.lensshift_v)1399+ ? ilogit(params[pcount++], -fit.lensshift_v_range, fit.lensshift_v_range)1400+ : fit.lensshift_v;1401+1402+ fit.lensshift_h = dt_isnan(fit.lensshift_h)1403+ ? ilogit(params[pcount++], -fit.lensshift_h_range, fit.lensshift_h_range)1404+ : fit.lensshift_h;1405+1406+ fit.shear = dt_isnan(fit.shear)1407+ ? ilogit(params[pcount++], -fit.shear_range, fit.shear_range)1408+ : fit.shear;1409+1410+#ifdef ASHIFT_DEBUG1411+ printf("params after optimization (%d iterations): rotation %f,"1412+ " lensshift_v %f, lensshift_h %f, shear %f\n",1413+ iter, fit.rotation, fit.lensshift_v, fit.lensshift_h, fit.shear);1414+#endif1415+1416+ // sanity check: in case of extreme values the image gets distorted1417+ // so strongly that it spans an insanely huge area. we check that1418+ // case and assume values that increase the image area by more than1419+ // a factor of 4 as being insane.1420+ float DT_ALIGNED_ARRAY homograph[3][3];1421+ _homography((float *)homograph, fit.rotation, fit.lensshift_v, fit.lensshift_h,1422+ fit.shear, fit.f_length_kb,1423+ fit.orthocorr, fit.aspect, fit.width, fit.height, ASHIFT_HOMOGRAPH_FORWARD);1424+1425+ // visit all four corners and find maximum span1426+ float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX;1427+ for(int y = 0; y < fit.height; y += fit.height - 1)1428+ for(int x = 0; x < fit.width; x += fit.width - 1)1429+ {1430+ float DT_ALIGNED_PIXEL pi[3], DT_ALIGNED_PIXEL po[3];1431+ pi[0] = x;1432+ pi[1] = y;1433+ pi[2] = 1.0f;1434+ mat3mulv(po, (float *)homograph, pi);1435+ po[0] /= po[2];1436+ po[1] /= po[2];1437+ xm = MIN(xm, po[0]);1438+ ym = MIN(ym, po[1]);1439+ xM = MAX(xM, po[0]);1440+ yM = MAX(yM, po[1]);1441+ }1442+1443+ if((xM - xm) * (yM - ym) > 4.0f * fit.width * fit.height)1444+ {1445+#ifdef ASHIFT_DEBUG1446+ printf("optimization not successful: degenerate case with"1447+ " area growth factor (%f) exceeding limits\n",1448+ (xM - xm) * (yM - ym) / (fit.width * fit.height));1449+#endif1450+ return NMS_INSANE;1451+ }1452+1453+ // now write the results into structure p1454+ p->rotation = fit.rotation;1455+ p->lensshift_v = fit.lensshift_v;1456+ p->lensshift_h = fit.lensshift_h;1457+ p->shear = fit.shear;1458+ return NMS_SUCCESS;1459+}1460+
A native/lsd.inc +2283 -0
@@ -0,0 +1,2283 @@
1+/*2+ This file is part of darktable,3+ Copyright (C) 2016-2025 darktable developers.4+5+ darktable is free software: you can redistribute it and/or modify6+ it under the terms of the GNU General Public License as published by7+ the Free Software Foundation, either version 3 of the License, or8+ (at your option) any later version.9+10+ darktable is distributed in the hope that it will be useful,11+ but WITHOUT ANY WARRANTY; without even the implied warranty of12+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13+ GNU General Public License for more details.14+15+ You should have received a copy of the GNU General Public License16+ along with darktable. If not, see <http://www.gnu.org/licenses/>.17+*/18+19+20+/* For line detection we are using the LSD code as published below.21+ * Changes versus the original code:22+ * do not include "lsd.h" (not needed)23+ * make all interface functions static24+ * comment out unused interface functions25+ * catch (unlikely) division by zero near line 203526+ * rename rad1 and rad2 to radius1 and radius2 in reduce_region_radius()27+ * to avoid naming conflict in windows build28+ *29+ */30+31+/* TODO: LSD employs intensive sanity checks of input data and allocation32+ * results. In case of errors it will terminate the program. On the one side33+ * this is not optimal for a module within darktable - it should better34+ * report errors upstream where they should be handled properly. On the other35+ * hand the kind of error which could be triggered in LSD would make it very unlikely36+ * that the darktable process could survive anyhow.37+ */38+39+// clang-format off40+41+/*==================================================================================42+ * begin LSD code version 1.6. downloaded on January 30, 201643+ *==================================================================================*/44+45+/*----------------------------------------------------------------------------46+47+ LSD - Line Segment Detector on digital images48+49+ This code is part of the following publication and was subject50+ to peer review:51+52+ "LSD: a Line Segment Detector" by Rafael Grompone von Gioi,53+ Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall,54+ Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd55+ http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd56+57+ Copyright (c) 2007-2011 rafael grompone von gioi <grompone@gmail.com>58+59+ This program is free software: you can redistribute it and/or modify60+ it under the terms of the GNU Affero General Public License as61+ published by the Free Software Foundation, either version 3 of the62+ License, or (at your option) any later version.63+64+ This program is distributed in the hope that it will be useful,65+ but WITHOUT ANY WARRANTY; without even the implied warranty of66+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the67+ GNU Affero General Public License for more details.68+69+ You should have received a copy of the GNU Affero General Public License70+ along with this program. If not, see <http://www.gnu.org/licenses/>.71+72+ ----------------------------------------------------------------------------*/73+74+/*----------------------------------------------------------------------------*/75+/** @file lsd.c76+ LSD module code77+ @author rafael grompone von gioi <grompone@gmail.com>78+ */79+/*----------------------------------------------------------------------------*/80+81+/*----------------------------------------------------------------------------*/82+/** @mainpage LSD code documentation83+84+ This is an implementation of the Line Segment Detector described85+ in the paper:86+87+ "LSD: A Fast Line Segment Detector with a False Detection Control"88+ by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel,89+ and Gregory Randall, IEEE Transactions on Pattern Analysis and90+ Machine Intelligence, vol. 32, no. 4, pp. 722-732, April, 2010.91+92+ and in more details in the CMLA Technical Report:93+94+ "LSD: A Line Segment Detector, Technical Report",95+ by Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel,96+ Gregory Randall, CMLA, ENS Cachan, 2010.97+98+ The version implemented here includes some further improvements99+ described in the following publication, of which this code is part:100+101+ "LSD: a Line Segment Detector" by Rafael Grompone von Gioi,102+ Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall,103+ Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd104+ http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd105+106+ The module's main function is lsd().107+108+ The source code is contained in two files: lsd.h and lsd.c.109+110+ HISTORY:111+ - version 1.6 - nov 2011:112+ - changes in the interface,113+ - max_grad parameter removed,114+ - the factor 11 was added to the number of test115+ to consider the different precision values116+ tested,117+ - a minor bug corrected in the gradient sorting118+ code,119+ - the algorithm now also returns p and log_nfa120+ for each detection,121+ - a minor bug was corrected in the image scaling,122+ - the angle comparison in "isaligned" changed123+ from < to <=,124+ - "eps" variable renamed "log_eps",125+ - "lsd_scale_region" interface was added,126+ - minor changes to comments.127+ - version 1.5 - dec 2010: Changes in 'refine', -W option added,128+ and more comments added.129+ - version 1.4 - jul 2010: lsd_scale interface added and doxygen doc.130+ - version 1.3 - feb 2010: Multiple bug correction and improved code.131+ - version 1.2 - dec 2009: First full Ansi C Language version.132+ - version 1.1 - sep 2009: Systematic subsampling to scale 0.8 and133+ correction to partially handle "angle problem".134+ - version 1.0 - jan 2009: First complete Megawave2 and Ansi C Language135+ version.136+137+ @author rafael grompone von gioi <grompone@gmail.com>138+ */139+/*----------------------------------------------------------------------------*/140+141+#include <stdlib.h>142+// math helpers supplied by compat.h143+144+#ifndef FALSE145+#define FALSE 0146+#endif /* !FALSE */147+148+#ifndef TRUE149+#define TRUE 1150+#endif /* !TRUE */151+152+/** Label for pixels with undefined gradient. */153+#define NOTDEF -1024.0154+155+/** 3/2 pi */156+#define M_3_2_PI M_PI * 3.0 / 2.0157+158+/** 2 pi */159+#define M_2__PI 2.0 * M_PI160+161+/** Label for pixels not used in yet. */162+#define NOTUSED 0163+164+/** Label for pixels already used in detection. */165+#define USED 1166+167+/*----------------------------------------------------------------------------*/168+/** Chained list of coordinates.169+ */170+struct coorlist171+{172+ int x,y;173+ struct coorlist * next;174+};175+176+/*----------------------------------------------------------------------------*/177+/** A point (or pixel).178+ */179+struct point {int x,y;};180+181+182+/*----------------------------------------------------------------------------*/183+/*------------------------- Miscellaneous functions --------------------------*/184+/*----------------------------------------------------------------------------*/185+186+/*----------------------------------------------------------------------------*/187+/** Fatal error, print a message to standard-error output and exit.188+ */189+static void error(char * msg)190+{191+ fprintf(stderr,"LSD Error: %s\n",msg);192+ exit(EXIT_FAILURE);193+}194+195+/*----------------------------------------------------------------------------*/196+/** Doubles relative error factor197+ */198+#define RELATIVE_ERROR_FACTOR 100.0199+200+/*----------------------------------------------------------------------------*/201+/** Compare doubles by relative error.202+203+ The resulting rounding error after floating point computations204+ depend on the specific operations done. The same number computed by205+ different algorithms could present different rounding errors. For a206+ useful comparison, an estimation of the relative rounding error207+ should be considered and compared to a factor times EPS. The factor208+ should be related to the cumulated rounding error in the chain of209+ computation. Here, as a simplification, a fixed factor is used.210+ */211+static int double_equal(const double a, const double b)212+{213+ double abs_diff,aa,bb,abs_max;214+215+ /* trivial case */216+ if( a == b ) return TRUE;217+218+ abs_diff = fabs(a-b);219+ aa = fabs(a);220+ bb = fabs(b);221+ abs_max = aa > bb ? aa : bb;222+223+ /* DBL_MIN is the smallest normalized number, thus, the smallest224+ number whose relative error is bounded by DBL_EPSILON. For225+ smaller numbers, the same quantization steps as for DBL_MIN226+ are used. Then, for smaller numbers, a meaningful "relative"227+ error should be computed by dividing the difference by DBL_MIN. */228+ if( abs_max < DBL_MIN ) abs_max = DBL_MIN;229+230+ /* equal if relative error <= factor x eps */231+ return (abs_diff / abs_max) <= (RELATIVE_ERROR_FACTOR * DBL_EPSILON);232+}233+234+/*----------------------------------------------------------------------------*/235+/** Computes Euclidean distance between point (x1,y1) and point (x2,y2).236+ */237+static double dist(const double x1, const double y1, const double x2, const double y2)238+{239+ return sqrt( (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) );240+}241+242+243+/*----------------------------------------------------------------------------*/244+/*----------------------- 'list of n-tuple' data type ------------------------*/245+/*----------------------------------------------------------------------------*/246+247+/*----------------------------------------------------------------------------*/248+/** 'list of n-tuple' data type249+250+ The i-th component of the j-th n-tuple of an n-tuple list 'ntl'251+ is accessed with:252+253+ ntl->values[ i + j * ntl->dim ]254+255+ The dimension of the n-tuple (n) is:256+257+ ntl->dim258+259+ The number of n-tuples in the list is:260+261+ ntl->size262+263+ The maximum number of n-tuples that can be stored in the264+ list with the allocated memory at a given time is given by:265+266+ ntl->max_size267+ */268+typedef struct ntuple_list_s269+{270+ unsigned int size;271+ unsigned int max_size;272+ unsigned int dim;273+ double * values;274+} * ntuple_list;275+276+/*----------------------------------------------------------------------------*/277+/** Free memory used in n-tuple 'in'.278+ */279+static void free_ntuple_list(const ntuple_list in)280+{281+ if( in == NULL || in->values == NULL )282+ error("free_ntuple_list: invalid n-tuple input.");283+ free( (void *) in->values );284+ free( (void *) in );285+}286+287+/*----------------------------------------------------------------------------*/288+/** Create an n-tuple list and allocate memory for one element.289+ @param dim the dimension (n) of the n-tuple.290+ */291+static ntuple_list new_ntuple_list(const unsigned int dim)292+{293+ ntuple_list n_tuple;294+295+ /* check parameters */296+ if( dim == 0 ) error("new_ntuple_list: 'dim' must be positive.");297+298+ /* get memory for list structure */299+ n_tuple = (ntuple_list) malloc( sizeof(struct ntuple_list_s) );300+ if( n_tuple == NULL ) error("not enough memory.");301+302+ /* initialize list */303+ n_tuple->size = 0;304+ n_tuple->max_size = 1;305+ n_tuple->dim = dim;306+307+ /* get memory for tuples */308+ n_tuple->values = (double *) malloc(sizeof(double) * dim * n_tuple->max_size);309+ if( n_tuple->values == NULL ) error("not enough memory.");310+311+ return n_tuple;312+}313+314+/*----------------------------------------------------------------------------*/315+/** Enlarge the allocated memory of an n-tuple list.316+ */317+static void enlarge_ntuple_list(const ntuple_list n_tuple)318+{319+ /* check parameters */320+ if( n_tuple == NULL || n_tuple->values == NULL || n_tuple->max_size == 0 )321+ error("enlarge_ntuple_list: invalid n-tuple.");322+323+ /* duplicate number of tuples */324+ n_tuple->max_size *= 2;325+326+ /* realloc memory */327+ n_tuple->values = (double *) realloc( (void *) n_tuple->values,328+ sizeof(double) * n_tuple->dim * n_tuple->max_size );329+ if( n_tuple->values == NULL ) error("not enough memory.");330+}331+332+/*----------------------------------------------------------------------------*/333+/** Add a 7-tuple to an n-tuple list.334+ */335+static void add_7tuple(const ntuple_list out, const double v1, const double v2, const double v3,336+ const double v4, const double v5, const double v6, const double v7 )337+{338+ /* check parameters */339+ if( out == NULL ) error("add_7tuple: invalid n-tuple input.");340+ if( out->dim != 7 ) error("add_7tuple: the n-tuple must be a 7-tuple.");341+342+ /* if needed, alloc more tuples to 'out' */343+ if( out->size == out->max_size ) enlarge_ntuple_list(out);344+ if( out->values == NULL ) error("add_7tuple: invalid n-tuple input.");345+346+ /* add new 7-tuple */347+ out->values[ out->size * out->dim + 0 ] = v1;348+ out->values[ out->size * out->dim + 1 ] = v2;349+ out->values[ out->size * out->dim + 2 ] = v3;350+ out->values[ out->size * out->dim + 3 ] = v4;351+ out->values[ out->size * out->dim + 4 ] = v5;352+ out->values[ out->size * out->dim + 5 ] = v6;353+ out->values[ out->size * out->dim + 6 ] = v7;354+355+ /* update number of tuples counter */356+ out->size++;357+}358+359+360+/*----------------------------------------------------------------------------*/361+/*----------------------------- Image Data Types -----------------------------*/362+/*----------------------------------------------------------------------------*/363+364+/*----------------------------------------------------------------------------*/365+/** char image data type366+367+ The pixel value at (x,y) is accessed by:368+369+ image->data[ x + y * image->xsize ]370+371+ with x and y integer.372+ */373+typedef struct image_char_s374+{375+ unsigned char * data;376+ unsigned int xsize,ysize;377+} * image_char;378+379+/*----------------------------------------------------------------------------*/380+/** Free memory used in image_char 'i'.381+ */382+static void free_image_char(const image_char i)383+{384+ if( i == NULL || i->data == NULL )385+ error("free_image_char: invalid input image.");386+ free( (void *) i->data );387+ free( (void *) i );388+}389+390+/*----------------------------------------------------------------------------*/391+/** Create a new image_char of size 'xsize' times 'ysize'.392+ */393+static image_char new_image_char(const unsigned int xsize, const unsigned int ysize)394+{395+ image_char image;396+397+ /* check parameters */398+ if( xsize == 0 || ysize == 0 ) error("new_image_char: invalid image size.");399+400+ /* get memory */401+ image = (image_char) malloc( sizeof(struct image_char_s) );402+ if( image == NULL ) error("not enough memory.");403+ image->data = (unsigned char *) calloc( (size_t) (xsize*ysize),404+ sizeof(unsigned char) );405+ if( image->data == NULL ) error("not enough memory.");406+407+ /* set image size */408+ image->xsize = xsize;409+ image->ysize = ysize;410+411+ return image;412+}413+414+/*----------------------------------------------------------------------------*/415+/** Create a new image_char of size 'xsize' times 'ysize',416+ initialized to the value 'fill_value'.417+ */418+static image_char new_image_char_ini(const unsigned int xsize, const unsigned int ysize,419+ const unsigned char fill_value )420+{421+ const image_char image = new_image_char(xsize,ysize); /* create image */422+ const unsigned int N = xsize*ysize;423+ unsigned int i;424+425+ /* check parameters */426+ if( image == NULL || image->data == NULL )427+ error("new_image_char_ini: invalid image.");428+429+ /* initialize */430+ for(i=0; i<N; i++) image->data[i] = fill_value;431+432+ return image;433+}434+435+/*----------------------------------------------------------------------------*/436+/** int image data type437+438+ The pixel value at (x,y) is accessed by:439+440+ image->data[ x + y * image->xsize ]441+442+ with x and y integer.443+ */444+typedef struct image_int_s445+{446+ int * data;447+ unsigned int xsize,ysize;448+} * image_int;449+450+/*----------------------------------------------------------------------------*/451+/** Create a new image_int of size 'xsize' times 'ysize'.452+ */453+static image_int new_image_int(const unsigned int xsize, const unsigned int ysize)454+{455+ image_int image;456+457+ /* check parameters */458+ if( xsize == 0 || ysize == 0 ) error("new_image_int: invalid image size.");459+460+ /* get memory */461+ image = (image_int) malloc( sizeof(struct image_int_s) );462+ if( image == NULL ) error("not enough memory.");463+ image->data = (int *) calloc( (size_t) (xsize*ysize), sizeof(int) );464+ if( image->data == NULL ) error("not enough memory.");465+466+ /* set image size */467+ image->xsize = xsize;468+ image->ysize = ysize;469+470+ return image;471+}472+473+/*----------------------------------------------------------------------------*/474+/** Create a new image_int of size 'xsize' times 'ysize',475+ initialized to the value 'fill_value'.476+ */477+static image_int new_image_int_ini(const unsigned int xsize, const unsigned int ysize,478+ const int fill_value )479+{480+ const image_int image = new_image_int(xsize,ysize); /* create image */481+ const unsigned int N = xsize*ysize;482+ unsigned int i;483+484+ /* initialize */485+ for(i=0; i<N; i++) image->data[i] = fill_value;486+487+ return image;488+}489+490+/*----------------------------------------------------------------------------*/491+/** double image data type492+493+ The pixel value at (x,y) is accessed by:494+495+ image->data[ x + y * image->xsize ]496+497+ with x and y integer.498+ */499+typedef struct image_double_s500+{501+ double * data;502+ unsigned int xsize,ysize;503+} * image_double;504+505+/*----------------------------------------------------------------------------*/506+/** Free memory used in image_double 'i'.507+ */508+static void free_image_double(const image_double i)509+{510+ if( i == NULL || i->data == NULL )511+ error("free_image_double: invalid input image.");512+ free( (void *) i->data );513+ free( (void *) i );514+}515+516+/*----------------------------------------------------------------------------*/517+/** Create a new image_double of size 'xsize' times 'ysize'.518+ */519+static image_double new_image_double(const unsigned int xsize, const unsigned int ysize)520+{521+ image_double image;522+523+ /* check parameters */524+ if( xsize == 0 || ysize == 0 ) error("new_image_double: invalid image size.");525+526+ /* get memory */527+ image = (image_double) malloc( sizeof(struct image_double_s) );528+ if( image == NULL ) error("not enough memory.");529+ image->data = (double *) calloc( (size_t) (xsize*ysize), sizeof(double) );530+ if( image->data == NULL ) error("not enough memory.");531+532+ /* set image size */533+ image->xsize = xsize;534+ image->ysize = ysize;535+536+ return image;537+}538+539+/*----------------------------------------------------------------------------*/540+/** Create a new image_double of size 'xsize' times 'ysize'541+ with the data pointed by 'data'.542+ */543+static image_double new_image_double_ptr(const unsigned int xsize,544+ const unsigned int ysize, double * data )545+{546+ image_double image;547+548+ /* check parameters */549+ if( xsize == 0 || ysize == 0 )550+ error("new_image_double_ptr: invalid image size.");551+ if( data == NULL ) error("new_image_double_ptr: NULL data pointer.");552+553+ /* get memory */554+ image = (image_double) malloc( sizeof(struct image_double_s) );555+ if( image == NULL ) error("not enough memory.");556+557+ /* set image */558+ image->xsize = xsize;559+ image->ysize = ysize;560+ image->data = data;561+562+ return image;563+}564+565+566+/*----------------------------------------------------------------------------*/567+/*----------------------------- Gaussian filter ------------------------------*/568+/*----------------------------------------------------------------------------*/569+570+/*----------------------------------------------------------------------------*/571+/** Compute a Gaussian kernel of length 'kernel->dim',572+ standard deviation 'sigma', and centered at value 'mean'.573+574+ For example, if mean=0.5, the Gaussian will be centered575+ in the middle point between values 'kernel->values[0]'576+ and 'kernel->values[1]'.577+ */578+static void gaussian_kernel(const ntuple_list kernel, const double sigma, const double mean)579+{580+ double sum = 0.0;581+ double val;582+ unsigned int i;583+584+ /* check parameters */585+ if( kernel == NULL || kernel->values == NULL )586+ error("gaussian_kernel: invalid n-tuple 'kernel'.");587+ if( sigma <= 0.0 ) error("gaussian_kernel: 'sigma' must be positive.");588+589+ /* compute Gaussian kernel */590+ if( kernel->max_size < 1 ) enlarge_ntuple_list(kernel);591+ kernel->size = 1;592+ for(i=0;i<kernel->dim;i++)593+ {594+ val = ( (double) i - mean ) / sigma;595+ kernel->values[i] = exp( -0.5 * val * val );596+ sum += kernel->values[i];597+ }598+599+ /* normalization */600+ if( sum >= 0.0 ) for(i=0;i<kernel->dim;i++) kernel->values[i] /= sum;601+}602+603+/*----------------------------------------------------------------------------*/604+/** Scale the input image 'in' by a factor 'scale' by Gaussian sub-sampling.605+606+ For example, scale=0.8 will give a result at 80% of the original size.607+608+ The image is convolved with a Gaussian kernel609+ @f[610+ G(x,y) = \frac{1}{2\pi\sigma^2} e^{-\frac{x^2+y^2}{2\sigma^2}}611+ @f]612+ before the sub-sampling to prevent aliasing.613+614+ The standard deviation sigma given by:615+ - sigma = sigma_scale / scale, if scale < 1.0616+ - sigma = sigma_scale, if scale >= 1.0617+618+ To be able to sub-sample at non-integer steps, some interpolation619+ is needed. In this implementation, the interpolation is done by620+ the Gaussian kernel, so both operations (filtering and sampling)621+ are done at the same time. The Gaussian kernel is computed622+ centered on the coordinates of the required sample. In this way,623+ when applied, it gives directly the result of convolving the image624+ with the kernel and interpolated to that particular position.625+626+ A fast algorithm is done using the separability of the Gaussian627+ kernel. Applying the 2D Gaussian kernel is equivalent to applying628+ first a horizontal 1D Gaussian kernel and then a vertical 1D629+ Gaussian kernel (or the other way round). The reason is that630+ @f[631+ G(x,y) = G(x) * G(y)632+ @f]633+ where634+ @f[635+ G(x) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{x^2}{2\sigma^2}}.636+ @f]637+ The algorithm first applies a combined Gaussian kernel and sampling638+ in the x axis, and then the combined Gaussian kernel and sampling639+ in the y axis.640+ */641+static image_double gaussian_sampler(const image_double in, const double scale,642+ const double sigma_scale )643+{644+ image_double aux,out;645+ ntuple_list kernel;646+ unsigned int N,M,h,n,x,y,i;647+ int xc,yc,j,double_x_size,double_y_size;648+ double sigma,xx,yy,sum,prec;649+650+ /* check parameters */651+ if( in == NULL || in->data == NULL || in->xsize == 0 || in->ysize == 0 )652+ error("gaussian_sampler: invalid image.");653+ if( scale <= 0.0 ) error("gaussian_sampler: 'scale' must be positive.");654+ if( sigma_scale <= 0.0 )655+ error("gaussian_sampler: 'sigma_scale' must be positive.");656+657+ /* compute new image size and get memory for images */658+ if( in->xsize * scale > (double) UINT_MAX ||659+ in->ysize * scale > (double) UINT_MAX )660+ error("gaussian_sampler: the output image size exceeds the handled size.");661+ N = (unsigned int) ceil( in->xsize * scale );662+ M = (unsigned int) ceil( in->ysize * scale );663+ aux = new_image_double(N,in->ysize);664+ out = new_image_double(N,M);665+666+ /* sigma, kernel size and memory for the kernel */667+ sigma = scale < 1.0 ? sigma_scale / scale : sigma_scale;668+ /*669+ The size of the kernel is selected to guarantee that the670+ the first discarded term is at least 10^prec times smaller671+ than the central value. For that, h should be larger than x, with672+ e^(-x^2/2sigma^2) = 1/10^prec.673+ Then,674+ x = sigma * sqrt( 2 * prec * ln(10) ).675+ */676+ prec = 3.0;677+ h = (unsigned int) ceil( sigma * sqrt( 2.0 * prec * log(10.0) ) );678+ n = 1+2*h; /* kernel size */679+ kernel = new_ntuple_list(n);680+681+ /* auxiliary double image size variables */682+ double_x_size = (int) (2 * in->xsize);683+ double_y_size = (int) (2 * in->ysize);684+685+ /* First subsampling: x axis */686+ for(x=0;x<aux->xsize;x++)687+ {688+ /*689+ x is the coordinate in the new image.690+ xx is the corresponding x-value in the original size image.691+ xc is the integer value, the pixel coordinate of xx.692+ */693+ xx = (double) x / scale;694+ /* coordinate (0.0,0.0) is in the center of pixel (0,0),695+ so the pixel with xc=0 get the values of xx from -0.5 to 0.5 */696+ xc = (int) floor( xx + 0.5 );697+ gaussian_kernel( kernel, sigma, (double) h + xx - (double) xc );698+ /* the kernel must be computed for each x because the fine699+ offset xx-xc is different in each case */700+701+ for(y=0;y<aux->ysize;y++)702+ {703+ sum = 0.0;704+ for(i=0;i<kernel->dim;i++)705+ {706+ j = xc - h + i;707+708+ /* symmetry boundary condition */709+ while( j < 0 ) j += double_x_size;710+ while( j >= double_x_size ) j -= double_x_size;711+ if( j >= (int) in->xsize ) j = double_x_size-1-j;712+713+ sum += in->data[ j + y * in->xsize ] * kernel->values[i];714+ }715+ aux->data[ x + y * aux->xsize ] = sum;716+ }717+ }718+719+ /* Second subsampling: y axis */720+ for(y=0;y<out->ysize;y++)721+ {722+ /*723+ y is the coordinate in the new image.724+ yy is the corresponding x-value in the original size image.725+ yc is the integer value, the pixel coordinate of xx.726+ */727+ yy = (double) y / scale;728+ /* coordinate (0.0,0.0) is in the center of pixel (0,0),729+ so the pixel with yc=0 get the values of yy from -0.5 to 0.5 */730+ yc = (int) floor( yy + 0.5 );731+ gaussian_kernel( kernel, sigma, (double) h + yy - (double) yc );732+ /* the kernel must be computed for each y because the fine733+ offset yy-yc is different in each case */734+735+ for(x=0;x<out->xsize;x++)736+ {737+ sum = 0.0;738+ for(i=0;i<kernel->dim;i++)739+ {740+ j = yc - h + i;741+742+ /* symmetry boundary condition */743+ while( j < 0 ) j += double_y_size;744+ while( j >= double_y_size ) j -= double_y_size;745+ if( j >= (int) in->ysize ) j = double_y_size-1-j;746+747+ sum += aux->data[ x + j * aux->xsize ] * kernel->values[i];748+ }749+ out->data[ x + y * out->xsize ] = sum;750+ }751+ }752+753+ /* free memory */754+ free_ntuple_list(kernel);755+ free_image_double(aux);756+757+ return out;758+}759+760+761+/*----------------------------------------------------------------------------*/762+/*--------------------------------- Gradient ---------------------------------*/763+/*----------------------------------------------------------------------------*/764+765+/*----------------------------------------------------------------------------*/766+/** Computes the direction of the level line of 'in' at each point.767+768+ The result is:769+ - an image_double with the angle at each pixel, or NOTDEF if not defined.770+ - the image_double 'modgrad' (a pointer is passed as argument)771+ with the gradient magnitude at each point.772+ - a list of pixels 'list_p' roughly ordered by decreasing773+ gradient magnitude. (The order is made by classifying points774+ into bins by gradient magnitude. The parameters 'n_bins' and775+ 'max_grad' specify the number of bins and the gradient modulus776+ at the highest bin. The pixels in the list would be in777+ decreasing gradient magnitude, up to a precision of the size of778+ the bins.)779+ - a pointer 'mem_p' to the memory used by 'list_p' to be able to780+ free the memory when it is not used anymore.781+ */782+static image_double ll_angle(const image_double in, const double threshold,783+ struct coorlist ** list_p, void ** mem_p,784+ image_double * modgrad, const unsigned int n_bins )785+{786+ image_double g;787+ unsigned int n,p,x,y,adr,i;788+ double com1,com2,gx,gy,norm,norm2;789+ /* the rest of the variables are used for pseudo-ordering790+ the gradient magnitude values */791+ int list_count = 0;792+ struct coorlist * list;793+ struct coorlist ** range_l_s; /* array of pointers to start of bin list */794+ struct coorlist ** range_l_e; /* array of pointers to end of bin list */795+ struct coorlist * start;796+ struct coorlist * end;797+ double max_grad = 0.0;798+799+ /* check parameters */800+ if( in == NULL || in->data == NULL || in->xsize == 0 || in->ysize == 0 )801+ error("ll_angle: invalid image.");802+ if( threshold < 0.0 ) error("ll_angle: 'threshold' must be positive.");803+ if( list_p == NULL ) error("ll_angle: NULL pointer 'list_p'.");804+ if( mem_p == NULL ) error("ll_angle: NULL pointer 'mem_p'.");805+ if( modgrad == NULL ) error("ll_angle: NULL pointer 'modgrad'.");806+ if( n_bins == 0 ) error("ll_angle: 'n_bins' must be positive.");807+808+ /* image size shortcuts */809+ n = in->ysize;810+ p = in->xsize;811+812+ /* allocate output image */813+ g = new_image_double(in->xsize,in->ysize);814+815+ /* get memory for the image of gradient modulus */816+ *modgrad = new_image_double(in->xsize,in->ysize);817+818+ /* get memory for "ordered" list of pixels */819+ list = (struct coorlist *) calloc( (size_t) (n*p), sizeof(struct coorlist) );820+ *mem_p = (void *) list;821+ range_l_s = (struct coorlist **) calloc( (size_t) n_bins,822+ sizeof(struct coorlist *) );823+ range_l_e = (struct coorlist **) calloc( (size_t) n_bins,824+ sizeof(struct coorlist *) );825+ if( list == NULL || range_l_s == NULL || range_l_e == NULL )826+ {827+ free(list);828+ free(range_l_s);829+ free(range_l_e);830+ error("not enough memory.");831+ }832+ for(i=0;i<n_bins;i++) range_l_s[i] = range_l_e[i] = NULL;833+834+ /* 'undefined' on the down and right boundaries */835+ for(x=0;x<p;x++) g->data[(n-1)*p+x] = NOTDEF;836+ for(y=0;y<n;y++) g->data[p*y+p-1] = NOTDEF;837+838+ /* compute gradient on the remaining pixels */839+ for(x=0;x<p-1;x++)840+ for(y=0;y<n-1;y++)841+ {842+ adr = y*p+x;843+844+ /*845+ Norm 2 computation using 2x2 pixel window:846+ A B847+ C D848+ and849+ com1 = D-A, com2 = B-C.850+ Then851+ gx = B+D - (A+C) horizontal difference852+ gy = C+D - (A+B) vertical difference853+ com1 and com2 are just to avoid 2 additions.854+ */855+ com1 = in->data[adr+p+1] - in->data[adr];856+ com2 = in->data[adr+1] - in->data[adr+p];857+858+ gx = com1+com2; /* gradient x component */859+ gy = com1-com2; /* gradient y component */860+ norm2 = gx*gx+gy*gy;861+ norm = sqrt( norm2 / 4.0 ); /* gradient norm */862+863+ (*modgrad)->data[adr] = norm; /* store gradient norm */864+865+ if( norm <= threshold ) /* norm too small, gradient no defined */866+ g->data[adr] = NOTDEF; /* gradient angle not defined */867+ else868+ {869+ /* gradient angle computation */870+ g->data[adr] = atan2(gx,-gy);871+872+ /* look for the maximum of the gradient */873+ if( norm > max_grad ) max_grad = norm;874+ }875+ }876+877+ /* compute histogram of gradient values */878+ for(x=0;x<p-1;x++)879+ for(y=0;y<n-1;y++)880+ {881+ norm = (*modgrad)->data[y*p+x];882+883+ /* store the point in the right bin according to its norm */884+ i = (unsigned int) (norm * (double) n_bins / max_grad);885+ if( i >= n_bins ) i = n_bins-1;886+ if( range_l_e[i] == NULL )887+ range_l_s[i] = range_l_e[i] = list+list_count++;888+ else889+ {890+ range_l_e[i]->next = list+list_count;891+ range_l_e[i] = list+list_count++;892+ }893+ range_l_e[i]->x = (int) x;894+ range_l_e[i]->y = (int) y;895+ range_l_e[i]->next = NULL;896+ }897+898+ /* Make the list of pixels (almost) ordered by norm value.899+ It starts by the larger bin, so the list starts by the900+ pixels with the highest gradient value. Pixels would be ordered901+ by norm value, up to a precision given by max_grad/n_bins.902+ */903+ for(i=n_bins-1; i>0 && range_l_s[i]==NULL; i--);904+ start = range_l_s[i];905+ end = range_l_e[i];906+ if( start != NULL )907+ while(i>0)908+ {909+ --i;910+ if( range_l_s[i] != NULL )911+ {912+ end->next = range_l_s[i];913+ end = range_l_e[i];914+ }915+ }916+ *list_p = start;917+918+ /* free memory */919+ free( (void *) range_l_s );920+ free( (void *) range_l_e );921+922+ return g;923+}924+925+/*----------------------------------------------------------------------------*/926+/** Is point (x,y) aligned to angle theta, up to precision 'prec'?927+ */928+static int isaligned(const int x, const int y, const image_double angles, double theta,929+ const double prec )930+{931+ double a;932+933+ /* check parameters */934+ if( angles == NULL || angles->data == NULL )935+ error("isaligned: invalid image 'angles'.");936+ if( x < 0 || y < 0 || x >= (int) angles->xsize || y >= (int) angles->ysize )937+ error("isaligned: (x,y) out of the image.");938+ if( prec < 0.0 ) error("isaligned: 'prec' must be positive.");939+940+ /* angle at pixel (x,y) */941+ a = angles->data[ x + y * angles->xsize ];942+943+ /* pixels whose level-line angle is not defined944+ are considered as NON-aligned */945+ if( a == NOTDEF ) return FALSE; /* there is no need to call the function946+ 'double_equal' here because there is947+ no risk of problems related to the948+ comparison doubles, we are only949+ interested in the exact NOTDEF value */950+951+ /* it is assumed that 'theta' and 'a' are in the range [-pi,pi] */952+ theta -= a;953+ if( theta < 0.0 ) theta = -theta;954+ if( theta > M_3_2_PI )955+ {956+ theta -= M_2__PI;957+ if( theta < 0.0 ) theta = -theta;958+ }959+960+ return theta <= prec;961+}962+963+/*----------------------------------------------------------------------------*/964+/** Absolute value angle difference.965+ */966+static double angle_diff(double a, const double b)967+{968+ a -= b;969+ while( a <= -M_PI ) a += M_2__PI;970+ while( a > M_PI ) a -= M_2__PI;971+ if( a < 0.0 ) a = -a;972+ return a;973+}974+975+/*----------------------------------------------------------------------------*/976+/** Signed angle difference.977+ */978+static double angle_diff_signed(double a, const double b)979+{980+ a -= b;981+ while( a <= -M_PI ) a += M_2__PI;982+ while( a > M_PI ) a -= M_2__PI;983+ return a;984+}985+986+987+/*----------------------------------------------------------------------------*/988+/*----------------------------- NFA computation ------------------------------*/989+/*----------------------------------------------------------------------------*/990+991+/*----------------------------------------------------------------------------*/992+/** Computes the natural logarithm of the absolute value of993+ the gamma function of x using the Lanczos approximation.994+ See http://www.rskey.org/gamma.htm995+996+ The formula used is997+ @f[998+ \Gamma(x) = \frac{ \sum_{n=0}^{N} q_n x^n }{ \Pi_{n=0}^{N} (x+n) }999+ (x+5.5)^{x+0.5} e^{-(x+5.5)}1000+ @f]1001+ so1002+ @f[1003+ \log\Gamma(x) = \log\left( \sum_{n=0}^{N} q_n x^n \right)1004+ + (x+0.5) \log(x+5.5) - (x+5.5) - \sum_{n=0}^{N} \log(x+n)1005+ @f]1006+ and1007+ q0 = 75122.6331530,1008+ q1 = 80916.6278952,1009+ q2 = 36308.2951477,1010+ q3 = 8687.24529705,1011+ q4 = 1168.92649479,1012+ q5 = 83.8676043424,1013+ q6 = 2.50662827511.1014+ */1015+static double log_gamma_lanczos(const double x)1016+{1017+ static double q[7] = { 75122.6331530, 80916.6278952, 36308.2951477,1018+ 8687.24529705, 1168.92649479, 83.8676043424,1019+ 2.50662827511 };1020+ double a = (x+0.5) * log(x+5.5) - (x+5.5);1021+ double b = 0.0;1022+ int n;1023+1024+ for(n=0;n<7;n++)1025+ {1026+ a -= log( x + (double) n );1027+ b += q[n] * pow( x, (double) n );1028+ }1029+ return a + log(b);1030+}1031+1032+/*----------------------------------------------------------------------------*/1033+/** Computes the natural logarithm of the absolute value of1034+ the gamma function of x using Windschitl method.1035+ See http://www.rskey.org/gamma.htm1036+1037+ The formula used is1038+ @f[1039+ \Gamma(x) = \sqrt{\frac{2\pi}{x}} \left( \frac{x}{e}1040+ \sqrt{ x\sinh(1/x) + \frac{1}{810x^6} } \right)^x1041+ @f]1042+ so1043+ @f[1044+ \log\Gamma(x) = 0.5\log(2\pi) + (x-0.5)\log(x) - x1045+ + 0.5x\log\left( x\sinh(1/x) + \frac{1}{810x^6} \right).1046+ @f]1047+ This formula is a good approximation when x > 15.1048+ */1049+static double log_gamma_windschitl(const double x)1050+{1051+ return 0.918938533204673 + (x-0.5)*log(x) - x1052+ + 0.5*x*log( x*sinh(1/x) + 1/(810.0*pow(x,6.0)) );1053+}1054+1055+/*----------------------------------------------------------------------------*/1056+/** Computes the natural logarithm of the absolute value of1057+ the gamma function of x. When x>15 use log_gamma_windschitl(),1058+ otherwise use log_gamma_lanczos().1059+ */1060+#define log_gamma(x) ((x)>15.0?log_gamma_windschitl(x):log_gamma_lanczos(x))1061+1062+/*----------------------------------------------------------------------------*/1063+/** Size of the table to store already computed inverse values.1064+ */1065+#define TABSIZE 1000001066+1067+// clang-format on1068+1069+static double *inv = NULL; /* table to keep computed inverse values */1070+1071+__attribute__((constructor)) static void invConstructor()1072+{1073+ if(inv) return;1074+ inv = malloc(sizeof(double) * TABSIZE);1075+}1076+1077+__attribute__((destructor)) static void invDestructor()1078+{1079+ free(inv);1080+ inv = NULL;1081+}1082+1083+// clang-format off1084+1085+/*----------------------------------------------------------------------------*/1086+/** Computes -log10(NFA).1087+1088+ NFA stands for Number of False Alarms:1089+ @f[1090+ \mathrm{NFA} = NT \cdot B(n,k,p)1091+ @f]1092+1093+ - NT - number of tests1094+ - B(n,k,p) - tail of binomial distribution with parameters n,k and p:1095+ @f[1096+ B(n,k,p) = \sum_{j=k}^n1097+ \left(\begin{array}{c}n\\j\end{array}\right)1098+ p^{j} (1-p)^{n-j}1099+ @f]1100+1101+ The value -log10(NFA) is equivalent but more intuitive than NFA:1102+ - -1 corresponds to 10 mean false alarms1103+ - 0 corresponds to 1 mean false alarm1104+ - 1 corresponds to 0.1 mean false alarms1105+ - 2 corresponds to 0.01 mean false alarms1106+ - ...1107+1108+ Used this way, the bigger the value, better the detection,1109+ and a logarithmic scale is used.1110+1111+ @param n,k,p binomial parameters.1112+ @param logNT logarithm of Number of Tests1113+1114+ The computation is based in the gamma function by the following1115+ relation:1116+ @f[1117+ \left(\begin{array}{c}n\\k\end{array}\right)1118+ = \frac{ \Gamma(n+1) }{ \Gamma(k+1) \cdot \Gamma(n-k+1) }.1119+ @f]1120+ We use efficient algorithms to compute the logarithm of1121+ the gamma function.1122+1123+ To make the computation faster, not all the sum is computed, part1124+ of the terms are neglected based on a bound to the error obtained1125+ (an error of 10% in the result is accepted).1126+ */1127+static double nfa(const int n, const int k, const double p, const double logNT)1128+{1129+ const double tolerance = 0.1; /* an error of 10% in the result is accepted */1130+ double log1term,term,bin_term,mult_term,bin_tail,err,p_term;1131+ int i;1132+1133+ /* check parameters */1134+ if( n<0 || k<0 || k>n || p<=0.0 || p>=1.0 )1135+ error("nfa: wrong n, k or p values.");1136+1137+ /* trivial cases */1138+ if( n==0 || k==0 ) return -logNT;1139+ if( n==k ) return -logNT - (double) n * log10(p);1140+1141+ /* probability term */1142+ p_term = p / (1.0-p);1143+1144+ /* compute the first term of the series */1145+ /*1146+ binomial_tail(n,k,p) = sum_{i=k}^n bincoef(n,i) * p^i * (1-p)^{n-i}1147+ where bincoef(n,i) are the binomial coefficients.1148+ But1149+ bincoef(n,k) = gamma(n+1) / ( gamma(k+1) * gamma(n-k+1) ).1150+ We use this to compute the first term. Actually the log of it.1151+ */1152+ log1term = log_gamma( (double) n + 1.0 ) - log_gamma( (double) k + 1.0 )1153+ - log_gamma( (double) (n-k) + 1.0 )1154+ + (double) k * log(p) + (double) (n-k) * log(1.0-p);1155+ term = exp(log1term);1156+1157+ /* in some cases no more computations are needed */1158+ if( double_equal(term,0.0) ) /* the first term is almost zero */1159+ {1160+ if( (double) k > (double) n * p ) /* at begin or end of the tail? */1161+ return -log1term / M_LN10 - logNT; /* end: use just the first term */1162+ else1163+ return -logNT; /* begin: the tail is roughly 1 */1164+ }1165+1166+ /* compute more terms if needed */1167+ bin_tail = term;1168+ for(i=k+1;i<=n;i++)1169+ {1170+ /*1171+ As1172+ term_i = bincoef(n,i) * p^i * (1-p)^(n-i)1173+ and1174+ bincoef(n,i)/bincoef(n,i-1) = n-1+1 / i,1175+ then,1176+ term_i / term_i-1 = (n-i+1)/i * p/(1-p)1177+ and1178+ term_i = term_i-1 * (n-i+1)/i * p/(1-p).1179+ 1/i is stored in a table as they are computed,1180+ because divisions are expensive.1181+ p/(1-p) is computed only once and stored in 'p_term'.1182+ */1183+ bin_term = (double) (n-i+1) * ( i<TABSIZE ?1184+ ( inv[i]!=0.0 ? inv[i] : ( inv[i] = 1.0 / (double) i ) ) :1185+ 1.0 / (double) i );1186+1187+ mult_term = bin_term * p_term;1188+ term *= mult_term;1189+ bin_tail += term;1190+ if(bin_term<1.0)1191+ {1192+ /* When bin_term<1 then mult_term_j<mult_term_i for j>i.1193+ Then, the error on the binomial tail when truncated at1194+ the i term can be bounded by a geometric series of form1195+ term_i * sum mult_term_i^j. */1196+ err = term * ( ( 1.0 - pow( mult_term, (double) (n-i+1) ) ) /1197+ (1.0-mult_term) - 1.0 );1198+1199+ /* One wants an error at most of tolerance*final_result, or:1200+ tolerance * abs(-log10(bin_tail)-logNT).1201+ Now, the error that can be accepted on bin_tail is1202+ given by tolerance*final_result divided by the derivative1203+ of -log10(x) when x=bin_tail. that is:1204+ tolerance * abs(-log10(bin_tail)-logNT) / (1/bin_tail)1205+ Finally, we truncate the tail if the error is less than:1206+ tolerance * abs(-log10(bin_tail)-logNT) * bin_tail */1207+ if( err < tolerance * fabs(-log10(bin_tail)-logNT) * bin_tail ) break;1208+ }1209+ }1210+ return -log10(bin_tail) - logNT;1211+}1212+1213+1214+/*----------------------------------------------------------------------------*/1215+/*--------------------------- Rectangle structure ----------------------------*/1216+/*----------------------------------------------------------------------------*/1217+1218+/*----------------------------------------------------------------------------*/1219+/** Rectangle structure: line segment with width.1220+ */1221+struct rect1222+{1223+ double x1,y1,x2,y2; /* first and second point of the line segment */1224+ double width; /* rectangle width */1225+ double x,y; /* center of the rectangle */1226+ double theta; /* angle */1227+ double dx,dy; /* (dx,dy) is vector oriented as the line segment */1228+ double prec; /* tolerance angle */1229+ double p; /* probability of a point with angle within 'prec' */1230+};1231+1232+/*----------------------------------------------------------------------------*/1233+/** Copy one rectangle structure to another.1234+ */1235+static void rect_copy(const struct rect * in, struct rect * out)1236+{1237+ /* check parameters */1238+ if( in == NULL || out == NULL ) error("rect_copy: invalid 'in' or 'out'.");1239+1240+ /* copy values */1241+ out->x1 = in->x1;1242+ out->y1 = in->y1;1243+ out->x2 = in->x2;1244+ out->y2 = in->y2;1245+ out->width = in->width;1246+ out->x = in->x;1247+ out->y = in->y;1248+ out->theta = in->theta;1249+ out->dx = in->dx;1250+ out->dy = in->dy;1251+ out->prec = in->prec;1252+ out->p = in->p;1253+}1254+1255+/*----------------------------------------------------------------------------*/1256+/** Rectangle points iterator.1257+1258+ The integer coordinates of pixels inside a rectangle are1259+ iteratively explored. This structure keep track of the process and1260+ functions ri_ini(), ri_inc(), ri_end(), and ri_del() are used in1261+ the process. An example of how to use the iterator is as follows:1262+ \code1263+1264+ struct rect * rec = XXX; // some rectangle1265+ rect_iter * i;1266+ for( i=ri_ini(rec); !ri_end(i); ri_inc(i) )1267+ {1268+ // your code, using 'i->x' and 'i->y' as coordinates1269+ }1270+ ri_del(i); // delete iterator1271+1272+ \endcode1273+ The pixels are explored 'column' by 'column', where we call1274+ 'column' a set of pixels with the same x value that are inside the1275+ rectangle. The following is an schematic representation of a1276+ rectangle, the 'column' being explored is marked by colons, and1277+ the current pixel being explored is 'x,y'.1278+ \verbatim1279+1280+ vx[1],vy[1]1281+ * *1282+ * *1283+ * *1284+ * ye1285+ * : *1286+ vx[0],vy[0] : *1287+ * : *1288+ * x,y *1289+ * : *1290+ * : vx[2],vy[2]1291+ * : *1292+ y ys *1293+ ^ * *1294+ | * *1295+ | * *1296+ +---> x vx[3],vy[3]1297+1298+ \endverbatim1299+ The first 'column' to be explored is the one with the smaller x1300+ value. Each 'column' is explored starting from the pixel of the1301+ 'column' (inside the rectangle) with the smallest y value.1302+1303+ The four corners of the rectangle are stored in order that rotates1304+ around the corners at the arrays 'vx[]' and 'vy[]'. The first1305+ point is always the one with smaller x value.1306+1307+ 'x' and 'y' are the coordinates of the pixel being explored. 'ys'1308+ and 'ye' are the start and end values of the current column being1309+ explored. So, 'ys' < 'ye'.1310+ */1311+typedef struct1312+{1313+ double vx[4]; /* rectangle's corner X coordinates in circular order */1314+ double vy[4]; /* rectangle's corner Y coordinates in circular order */1315+ double ys,ye; /* start and end Y values of current 'column' */1316+ int x,y; /* coordinates of currently explored pixel */1317+} rect_iter;1318+1319+/*----------------------------------------------------------------------------*/1320+/** Interpolate y value corresponding to 'x' value given, in1321+ the line 'x1,y1' to 'x2,y2'; if 'x1=x2' return the smaller1322+ of 'y1' and 'y2'.1323+1324+ The following restrictions are required:1325+ - x1 <= x21326+ - x1 <= x1327+ - x <= x21328+ */1329+static double inter_low(const double x, const double x1, const double y1, const double x2, const double y2)1330+{1331+ /* check parameters */1332+ if( x1 > x2 || x < x1 || x > x2 )1333+ error("inter_low: unsuitable input, 'x1>x2' or 'x<x1' or 'x>x2'.");1334+1335+ /* interpolation */1336+ if( double_equal(x1,x2) && y1<y2 ) return y1;1337+ if( double_equal(x1,x2) && y1>y2 ) return y2;1338+ return y1 + (x-x1) * (y2-y1) / (x2-x1);1339+}1340+1341+/*----------------------------------------------------------------------------*/1342+/** Interpolate y value corresponding to 'x' value given, in1343+ the line 'x1,y1' to 'x2,y2'; if 'x1=x2' return the larger1344+ of 'y1' and 'y2'.1345+1346+ The following restrictions are required:1347+ - x1 <= x21348+ - x1 <= x1349+ - x <= x21350+ */1351+static double inter_hi(const double x, const double x1, const double y1, const double x2, const double y2)1352+{1353+ /* check parameters */1354+ if( x1 > x2 || x < x1 || x > x2 )1355+ error("inter_hi: unsuitable input, 'x1>x2' or 'x<x1' or 'x>x2'.");1356+1357+ /* interpolation */1358+ if( double_equal(x1,x2) && y1<y2 ) return y2;1359+ if( double_equal(x1,x2) && y1>y2 ) return y1;1360+ return y1 + (x-x1) * (y2-y1) / (x2-x1);1361+}1362+1363+/*----------------------------------------------------------------------------*/1364+/** Free memory used by a rectangle iterator.1365+ */1366+static void ri_del(rect_iter * iter)1367+{1368+ if( iter == NULL ) error("ri_del: NULL iterator.");1369+ free( (void *) iter );1370+}1371+1372+/*----------------------------------------------------------------------------*/1373+/** Check if the iterator finished the full iteration.1374+1375+ See details in \ref rect_iter1376+ */1377+static int ri_end(const rect_iter * i)1378+{1379+ /* check input */1380+ if( i == NULL ) error("ri_end: NULL iterator.");1381+1382+ /* if the current x value is larger than the largest1383+ x value in the rectangle (vx[2]), we know the full1384+ exploration of the rectangle is finished. */1385+ return (double)(i->x) > i->vx[2];1386+}1387+1388+/*----------------------------------------------------------------------------*/1389+/** Increment a rectangle iterator.1390+1391+ See details in \ref rect_iter1392+ */1393+static void ri_inc(rect_iter * i)1394+{1395+ /* check input */1396+ if( i == NULL ) error("ri_inc: NULL iterator.");1397+1398+ /* if not at end of exploration,1399+ increase y value for next pixel in the 'column' */1400+ if( !ri_end(i) ) i->y++;1401+1402+ /* if the end of the current 'column' is reached,1403+ and it is not the end of exploration,1404+ advance to the next 'column' */1405+ while( (double) (i->y) > i->ye && !ri_end(i) )1406+ {1407+ /* increase x, next 'column' */1408+ i->x++;1409+1410+ /* if end of exploration, return */1411+ if( ri_end(i) ) return;1412+1413+ /* update lower y limit (start) for the new 'column'.1414+1415+ We need to interpolate the y value that corresponds to the1416+ lower side of the rectangle. The first thing is to decide if1417+ the corresponding side is1418+1419+ vx[0],vy[0] to vx[3],vy[3] or1420+ vx[3],vy[3] to vx[2],vy[2]1421+1422+ Then, the side is interpolated for the x value of the1423+ 'column'. But, if the side is vertical (as it could happen if1424+ the rectangle is vertical and we are dealing with the first1425+ or last 'columns') then we pick the lower value of the side1426+ by using 'inter_low'.1427+ */1428+ if( (double) i->x < i->vx[3] )1429+ i->ys = inter_low((double)i->x,i->vx[0],i->vy[0],i->vx[3],i->vy[3]);1430+ else1431+ i->ys = inter_low((double)i->x,i->vx[3],i->vy[3],i->vx[2],i->vy[2]);1432+1433+ /* update upper y limit (end) for the new 'column'.1434+1435+ We need to interpolate the y value that corresponds to the1436+ upper side of the rectangle. The first thing is to decide if1437+ the corresponding side is1438+1439+ vx[0],vy[0] to vx[1],vy[1] or1440+ vx[1],vy[1] to vx[2],vy[2]1441+1442+ Then, the side is interpolated for the x value of the1443+ 'column'. But, if the side is vertical (as it could happen if1444+ the rectangle is vertical and we are dealing with the first1445+ or last 'columns') then we pick the lower value of the side1446+ by using 'inter_low'.1447+ */1448+ if( (double)i->x < i->vx[1] )1449+ i->ye = inter_hi((double)i->x,i->vx[0],i->vy[0],i->vx[1],i->vy[1]);1450+ else1451+ i->ye = inter_hi((double)i->x,i->vx[1],i->vy[1],i->vx[2],i->vy[2]);1452+1453+ /* new y */1454+ i->y = (int) ceil(i->ys);1455+ }1456+}1457+1458+/*----------------------------------------------------------------------------*/1459+/** Create and initialize a rectangle iterator.1460+1461+ See details in \ref rect_iter1462+ */1463+static rect_iter * ri_ini(const struct rect * r)1464+{1465+ double vx[4],vy[4];1466+ int n,offset;1467+ rect_iter * i;1468+1469+ /* check parameters */1470+ if( r == NULL ) error("ri_ini: invalid rectangle.");1471+1472+ /* get memory */1473+ i = (rect_iter *) malloc(sizeof(rect_iter));1474+ if( i == NULL ) error("ri_ini: Not enough memory.");1475+1476+ /* build list of rectangle corners ordered1477+ in a circular way around the rectangle */1478+ vx[0] = r->x1 - r->dy * r->width / 2.0;1479+ vy[0] = r->y1 + r->dx * r->width / 2.0;1480+ vx[1] = r->x2 - r->dy * r->width / 2.0;1481+ vy[1] = r->y2 + r->dx * r->width / 2.0;1482+ vx[2] = r->x2 + r->dy * r->width / 2.0;1483+ vy[2] = r->y2 - r->dx * r->width / 2.0;1484+ vx[3] = r->x1 + r->dy * r->width / 2.0;1485+ vy[3] = r->y1 - r->dx * r->width / 2.0;1486+1487+ /* compute rotation of index of corners needed so that the first1488+ point has the smaller x.1489+1490+ if one side is vertical, thus two corners have the same smaller x1491+ value, the one with the largest y value is selected as the first.1492+ */1493+ if( r->x1 < r->x2 && r->y1 <= r->y2 ) offset = 0;1494+ else if( r->x1 >= r->x2 && r->y1 < r->y2 ) offset = 1;1495+ else if( r->x1 > r->x2 && r->y1 >= r->y2 ) offset = 2;1496+ else offset = 3;1497+1498+ /* apply rotation of index. */1499+ for(n=0; n<4; n++)1500+ {1501+ i->vx[n] = vx[(offset+n)%4];1502+ i->vy[n] = vy[(offset+n)%4];1503+ }1504+1505+ /* Set an initial condition.1506+1507+ The values are set to values that will cause 'ri_inc' (that will1508+ be called immediately) to initialize correctly the first 'column'1509+ and compute the limits 'ys' and 'ye'.1510+1511+ 'y' is set to the integer value of vy[0], the starting corner.1512+1513+ 'ys' and 'ye' are set to very small values, so 'ri_inc' will1514+ notice that it needs to start a new 'column'.1515+1516+ The smallest integer coordinate inside of the rectangle is1517+ 'ceil(vx[0])'. The current 'x' value is set to that value minus1518+ one, so 'ri_inc' (that will increase x by one) will advance to1519+ the first 'column'.1520+ */1521+ i->x = (int) ceil(i->vx[0]) - 1;1522+ i->y = (int) ceil(i->vy[0]);1523+ i->ys = i->ye = -DBL_MAX;1524+1525+ /* advance to the first pixel */1526+ ri_inc(i);1527+1528+ return i;1529+}1530+1531+/*----------------------------------------------------------------------------*/1532+/** Compute a rectangle's NFA value.1533+ */1534+static double rect_nfa(const struct rect * rec, const image_double angles, const double logNT)1535+{1536+ rect_iter * i;1537+ int pts = 0;1538+ int alg = 0;1539+1540+ /* check parameters */1541+ if( rec == NULL ) error("rect_nfa: invalid rectangle.");1542+ if( angles == NULL ) error("rect_nfa: invalid 'angles'.");1543+1544+ /* compute the total number of pixels and of aligned points in 'rec' */1545+ for(i=ri_ini(rec); !ri_end(i); ri_inc(i)) /* rectangle iterator */1546+ if( i->x >= 0 && i->y >= 0 &&1547+ i->x < (int) angles->xsize && i->y < (int) angles->ysize )1548+ {1549+ ++pts; /* total number of pixels counter */1550+ if( isaligned(i->x, i->y, angles, rec->theta, rec->prec) )1551+ ++alg; /* aligned points counter */1552+ }1553+ ri_del(i); /* delete iterator */1554+1555+ return nfa(pts,alg,rec->p,logNT); /* compute NFA value */1556+}1557+1558+1559+/*----------------------------------------------------------------------------*/1560+/*---------------------------------- Regions ---------------------------------*/1561+/*----------------------------------------------------------------------------*/1562+1563+/*----------------------------------------------------------------------------*/1564+/** Compute region's angle as the principal inertia axis of the region.1565+1566+ The following is the region inertia matrix A:1567+ @f[1568+1569+ A = \left(\begin{array}{cc}1570+ Ixx & Ixy \\1571+ Ixy & Iyy \\1572+ \end{array}\right)1573+1574+ @f]1575+ where1576+1577+ Ixx = sum_i G(i).(y_i - cx)^21578+1579+ Iyy = sum_i G(i).(x_i - cy)^21580+1581+ Ixy = - sum_i G(i).(x_i - cx).(y_i - cy)1582+1583+ and1584+ - G(i) is the gradient norm at pixel i, used as pixel's weight.1585+ - x_i and y_i are the coordinates of pixel i.1586+ - cx and cy are the coordinates of the center of th region.1587+1588+ lambda1 and lambda2 are the eigenvalues of matrix A,1589+ with lambda1 >= lambda2. They are found by solving the1590+ characteristic polynomial:1591+1592+ det( lambda I - A) = 01593+1594+ that gives:1595+1596+ lambda1 = ( Ixx + Iyy + sqrt( (Ixx-Iyy)^2 + 4.0*Ixy*Ixy) ) / 21597+1598+ lambda2 = ( Ixx + Iyy - sqrt( (Ixx-Iyy)^2 + 4.0*Ixy*Ixy) ) / 21599+1600+ To get the line segment direction we want to get the angle the1601+ eigenvector associated to the smallest eigenvalue. We have1602+ to solve for a,b in:1603+1604+ a.Ixx + b.Ixy = a.lambda21605+1606+ a.Ixy + b.Iyy = b.lambda21607+1608+ We want the angle theta = atan(b/a). It can be computed with1609+ any of the two equations:1610+1611+ theta = atan( (lambda2-Ixx) / Ixy )1612+1613+ or1614+1615+ theta = atan( Ixy / (lambda2-Iyy) )1616+1617+ When |Ixx| > |Iyy| we use the first, otherwise the second (just to1618+ get better numeric precision).1619+ */1620+static double get_theta(const struct point * reg, const int reg_size, const double x, const double y,1621+ const image_double modgrad, const double reg_angle, const double prec )1622+{1623+ double lambda,theta,weight;1624+ double Ixx = 0.0;1625+ double Iyy = 0.0;1626+ double Ixy = 0.0;1627+ int i;1628+1629+ /* check parameters */1630+ if( reg == NULL ) error("get_theta: invalid region.");1631+ if( reg_size <= 1 ) error("get_theta: region size <= 1.");1632+ if( modgrad == NULL || modgrad->data == NULL )1633+ error("get_theta: invalid 'modgrad'.");1634+ if( prec < 0.0 ) error("get_theta: 'prec' must be positive.");1635+1636+ /* compute inertia matrix */1637+ for(i=0; i<reg_size; i++)1638+ {1639+ weight = modgrad->data[ reg[i].x + reg[i].y * modgrad->xsize ];1640+ Ixx += ( (double) reg[i].y - y ) * ( (double) reg[i].y - y ) * weight;1641+ Iyy += ( (double) reg[i].x - x ) * ( (double) reg[i].x - x ) * weight;1642+ Ixy -= ( (double) reg[i].x - x ) * ( (double) reg[i].y - y ) * weight;1643+ }1644+ if( double_equal(Ixx,0.0) && double_equal(Iyy,0.0) && double_equal(Ixy,0.0) )1645+ error("get_theta: null inertia matrix.");1646+1647+ /* compute smallest eigenvalue */1648+ lambda = 0.5 * ( Ixx + Iyy - sqrt( (Ixx-Iyy)*(Ixx-Iyy) + 4.0*Ixy*Ixy ) );1649+1650+ /* compute angle */1651+ theta = fabs(Ixx)>fabs(Iyy) ? atan2(lambda-Ixx,Ixy) : atan2(Ixy,lambda-Iyy);1652+1653+ /* The previous procedure doesn't cares about orientation,1654+ so it could be wrong by 180 degrees. Here is corrected if necessary. */1655+ if( angle_diff(theta,reg_angle) > prec ) theta += M_PI;1656+1657+ return theta;1658+}1659+1660+/*----------------------------------------------------------------------------*/1661+/** Computes a rectangle that covers a region of points.1662+ */1663+static void region2rect(const struct point * reg, const int reg_size,1664+ const image_double modgrad, const double reg_angle,1665+ const double prec, const double p, struct rect * rec )1666+{1667+ double x,y,dx,dy,l,w,theta,weight,sum,l_min,l_max,w_min,w_max;1668+ int i;1669+1670+ /* check parameters */1671+ if( reg == NULL ) error("region2rect: invalid region.");1672+ if( reg_size <= 1 ) error("region2rect: region size <= 1.");1673+ if( modgrad == NULL || modgrad->data == NULL )1674+ error("region2rect: invalid image 'modgrad'.");1675+ if( rec == NULL ) error("region2rect: invalid 'rec'.");1676+1677+ /* center of the region:1678+1679+ It is computed as the weighted sum of the coordinates1680+ of all the pixels in the region. The norm of the gradient1681+ is used as the weight of a pixel. The sum is as follows:1682+ cx = \sum_i G(i).x_i1683+ cy = \sum_i G(i).y_i1684+ where G(i) is the norm of the gradient of pixel i1685+ and x_i,y_i are its coordinates.1686+ */1687+ x = y = sum = 0.0;1688+ for(i=0; i<reg_size; i++)1689+ {1690+ weight = modgrad->data[ reg[i].x + reg[i].y * modgrad->xsize ];1691+ x += (double) reg[i].x * weight;1692+ y += (double) reg[i].y * weight;1693+ sum += weight;1694+ }1695+ if( sum <= 0.0 ) error("region2rect: weights sum equal to zero.");1696+ x /= sum;1697+ y /= sum;1698+1699+ /* theta */1700+ theta = get_theta(reg,reg_size,x,y,modgrad,reg_angle,prec);1701+1702+ /* length and width:1703+1704+ 'l' and 'w' are computed as the distance from the center of the1705+ region to pixel i, projected along the rectangle axis (dx,dy) and1706+ to the orthogonal axis (-dy,dx), respectively.1707+1708+ The length of the rectangle goes from l_min to l_max, where l_min1709+ and l_max are the minimum and maximum values of l in the region.1710+ Analogously, the width is selected from w_min to w_max, where1711+ w_min and w_max are the minimum and maximum of w for the pixels1712+ in the region.1713+ */1714+ dx = cos(theta);1715+ dy = sin(theta);1716+ l_min = l_max = w_min = w_max = 0.0;1717+ for(i=0; i<reg_size; i++)1718+ {1719+ l = ( (double) reg[i].x - x) * dx + ( (double) reg[i].y - y) * dy;1720+ w = -( (double) reg[i].x - x) * dy + ( (double) reg[i].y - y) * dx;1721+1722+ if( l > l_max ) l_max = l;1723+ if( l < l_min ) l_min = l;1724+ if( w > w_max ) w_max = w;1725+ if( w < w_min ) w_min = w;1726+ }1727+1728+ /* store values */1729+ rec->x1 = x + l_min * dx;1730+ rec->y1 = y + l_min * dy;1731+ rec->x2 = x + l_max * dx;1732+ rec->y2 = y + l_max * dy;1733+ rec->width = w_max - w_min;1734+ rec->x = x;1735+ rec->y = y;1736+ rec->theta = theta;1737+ rec->dx = dx;1738+ rec->dy = dy;1739+ rec->prec = prec;1740+ rec->p = p;1741+1742+ /* we impose a minimal width of one pixel1743+1744+ A sharp horizontal or vertical step would produce a perfectly1745+ horizontal or vertical region. The width computed would be1746+ zero. But that corresponds to a one pixels width transition in1747+ the image.1748+ */1749+ if( rec->width < 1.0 ) rec->width = 1.0;1750+}1751+1752+/*----------------------------------------------------------------------------*/1753+/** Build a region of pixels that share the same angle, up to a1754+ tolerance 'prec', starting at point (x,y).1755+ */1756+static void region_grow(const int x, const int y, const image_double angles, struct point * reg,1757+ int * reg_size, double * reg_angle, const image_char used,1758+ const double prec )1759+{1760+ double sumdx,sumdy;1761+ int xx,yy,i;1762+1763+ /* check parameters */1764+ if( x < 0 || y < 0 || x >= (int) angles->xsize || y >= (int) angles->ysize )1765+ error("region_grow: (x,y) out of the image.");1766+ if( angles == NULL || angles->data == NULL )1767+ error("region_grow: invalid image 'angles'.");1768+ if( reg == NULL ) error("region_grow: invalid 'reg'.");1769+ if( reg_size == NULL ) error("region_grow: invalid pointer 'reg_size'.");1770+ if( reg_angle == NULL ) error("region_grow: invalid pointer 'reg_angle'.");1771+ if( used == NULL || used->data == NULL )1772+ error("region_grow: invalid image 'used'.");1773+1774+ /* first point of the region */1775+ *reg_size = 1;1776+ reg[0].x = x;1777+ reg[0].y = y;1778+ *reg_angle = angles->data[x+y*angles->xsize]; /* region's angle */1779+ sumdx = cos(*reg_angle);1780+ sumdy = sin(*reg_angle);1781+ used->data[x+y*used->xsize] = USED;1782+1783+ /* try neighbors as new region points */1784+ for(i=0; i<*reg_size; i++)1785+ for(xx=reg[i].x-1; xx<=reg[i].x+1; xx++)1786+ for(yy=reg[i].y-1; yy<=reg[i].y+1; yy++)1787+ if( xx>=0 && yy>=0 && xx<(int)used->xsize && yy<(int)used->ysize &&1788+ used->data[xx+yy*used->xsize] != USED &&1789+ isaligned(xx,yy,angles,*reg_angle,prec) )1790+ {1791+ /* add point */1792+ used->data[xx+yy*used->xsize] = USED;1793+ reg[*reg_size].x = xx;1794+ reg[*reg_size].y = yy;1795+ ++(*reg_size);1796+1797+ /* update region's angle */1798+ sumdx += cos( angles->data[xx+yy*angles->xsize] );1799+ sumdy += sin( angles->data[xx+yy*angles->xsize] );1800+ *reg_angle = atan2(sumdy,sumdx);1801+ }1802+}1803+1804+/*----------------------------------------------------------------------------*/1805+/** Try some rectangles variations to improve NFA value. Only if the1806+ rectangle is not meaningful (i.e., log_nfa <= log_eps).1807+ */1808+static double rect_improve( struct rect * rec, const image_double angles,1809+ const double logNT, const double log_eps )1810+{1811+ struct rect r;1812+ double log_nfa,log_nfa_new;1813+ const double delta = 0.5;1814+ const double delta_2 = delta / 2.0;1815+ int n;1816+1817+ log_nfa = rect_nfa(rec,angles,logNT);1818+1819+ if( log_nfa > log_eps ) return log_nfa;1820+1821+ /* try finer precisions */1822+ rect_copy(rec,&r);1823+ for(n=0; n<5; n++)1824+ {1825+ r.p /= 2.0;1826+ r.prec = r.p * M_PI;1827+ log_nfa_new = rect_nfa(&r,angles,logNT);1828+ if( log_nfa_new > log_nfa )1829+ {1830+ log_nfa = log_nfa_new;1831+ rect_copy(&r,rec);1832+ }1833+ }1834+1835+ if( log_nfa > log_eps ) return log_nfa;1836+1837+ /* try to reduce width */1838+ rect_copy(rec,&r);1839+ for(n=0; n<5; n++)1840+ {1841+ if( (r.width - delta) >= 0.5 )1842+ {1843+ r.width -= delta;1844+ log_nfa_new = rect_nfa(&r,angles,logNT);1845+ if( log_nfa_new > log_nfa )1846+ {1847+ rect_copy(&r,rec);1848+ log_nfa = log_nfa_new;1849+ }1850+ }1851+ }1852+1853+ if( log_nfa > log_eps ) return log_nfa;1854+1855+ /* try to reduce one side of the rectangle */1856+ rect_copy(rec,&r);1857+ for(n=0; n<5; n++)1858+ {1859+ if( (r.width - delta) >= 0.5 )1860+ {1861+ r.x1 += -r.dy * delta_2;1862+ r.y1 += r.dx * delta_2;1863+ r.x2 += -r.dy * delta_2;1864+ r.y2 += r.dx * delta_2;1865+ r.width -= delta;1866+ log_nfa_new = rect_nfa(&r,angles,logNT);1867+ if( log_nfa_new > log_nfa )1868+ {1869+ rect_copy(&r,rec);1870+ log_nfa = log_nfa_new;1871+ }1872+ }1873+ }1874+1875+ if( log_nfa > log_eps ) return log_nfa;1876+1877+ /* try to reduce the other side of the rectangle */1878+ rect_copy(rec,&r);1879+ for(n=0; n<5; n++)1880+ {1881+ if( (r.width - delta) >= 0.5 )1882+ {1883+ r.x1 -= -r.dy * delta_2;1884+ r.y1 -= r.dx * delta_2;1885+ r.x2 -= -r.dy * delta_2;1886+ r.y2 -= r.dx * delta_2;1887+ r.width -= delta;1888+ log_nfa_new = rect_nfa(&r,angles,logNT);1889+ if( log_nfa_new > log_nfa )1890+ {1891+ rect_copy(&r,rec);1892+ log_nfa = log_nfa_new;1893+ }1894+ }1895+ }1896+1897+ if( log_nfa > log_eps ) return log_nfa;1898+1899+ /* try even finer precisions */1900+ rect_copy(rec,&r);1901+ for(n=0; n<5; n++)1902+ {1903+ r.p /= 2.0;1904+ r.prec = r.p * M_PI;1905+ log_nfa_new = rect_nfa(&r,angles,logNT);1906+ if( log_nfa_new > log_nfa )1907+ {1908+ log_nfa = log_nfa_new;1909+ rect_copy(&r,rec);1910+ }1911+ }1912+1913+ return log_nfa;1914+}1915+1916+/*----------------------------------------------------------------------------*/1917+/** Reduce the region size, by elimination the points far from the1918+ starting point, until that leads to rectangle with the right1919+ density of region points or to discard the region if too small.1920+ */1921+static int reduce_region_radius( struct point * reg, int * reg_size,1922+ const image_double modgrad, const double reg_angle,1923+ const double prec, const double p, struct rect * rec,1924+ const image_char used, const image_double angles,1925+ const double density_th )1926+{1927+ double density,radius1,radius2,rad,xc,yc;1928+ int i;1929+1930+ /* check parameters */1931+ if( reg == NULL ) error("reduce_region_radius: invalid pointer 'reg'.");1932+ if( reg_size == NULL )1933+ error("reduce_region_radius: invalid pointer 'reg_size'.");1934+ if( prec < 0.0 ) error("reduce_region_radius: 'prec' must be positive.");1935+ if( rec == NULL ) error("reduce_region_radius: invalid pointer 'rec'.");1936+ if( used == NULL || used->data == NULL )1937+ error("reduce_region_radius: invalid image 'used'.");1938+ if( angles == NULL || angles->data == NULL )1939+ error("reduce_region_radius: invalid image 'angles'.");1940+1941+ /* compute region points density */1942+ density = (double) *reg_size /1943+ ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width );1944+1945+ /* if the density criterion is satisfied there is nothing to do */1946+ if( density >= density_th ) return TRUE;1947+1948+ /* compute region's radius */1949+ xc = (double) reg[0].x;1950+ yc = (double) reg[0].y;1951+ radius1 = dist( xc, yc, rec->x1, rec->y1 );1952+ radius2 = dist( xc, yc, rec->x2, rec->y2 );1953+ rad = radius1 > radius2 ? radius1 : radius2;1954+1955+ /* while the density criterion is not satisfied, remove farther pixels */1956+ while( density < density_th )1957+ {1958+ rad *= 0.75; /* reduce region's radius to 75% of its value */1959+1960+ /* remove points from the region and update 'used' map */1961+ for(i=0; i<*reg_size; i++)1962+ if( dist( xc, yc, (double) reg[i].x, (double) reg[i].y ) > rad )1963+ {1964+ /* point not kept, mark it as NOTUSED */1965+ used->data[ reg[i].x + reg[i].y * used->xsize ] = NOTUSED;1966+ /* remove point from the region */1967+ reg[i].x = reg[*reg_size-1].x; /* if i==*reg_size-1 copy itself */1968+ reg[i].y = reg[*reg_size-1].y;1969+ --(*reg_size);1970+ --i; /* to avoid skipping one point */1971+ }1972+1973+ /* reject if the region is too small.1974+ 2 is the minimal region size for 'region2rect' to work. */1975+ if( *reg_size < 2 ) return FALSE;1976+1977+ /* re-compute rectangle */1978+ region2rect(reg,*reg_size,modgrad,reg_angle,prec,p,rec);1979+1980+ /* re-compute region points density */1981+ density = (double) *reg_size /1982+ ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width );1983+ }1984+1985+ /* if this point is reached, the density criterion is satisfied */1986+ return TRUE;1987+}1988+1989+/*----------------------------------------------------------------------------*/1990+/** Refine a rectangle.1991+1992+ For that, an estimation of the angle tolerance is performed by the1993+ standard deviation of the angle at points near the region's1994+ starting point. Then, a new region is grown starting from the same1995+ point, but using the estimated angle tolerance. If this fails to1996+ produce a rectangle with the right density of region points,1997+ 'reduce_region_radius' is called to try to satisfy this condition.1998+ */1999+static int refine( struct point * reg, int * reg_size, const image_double modgrad,2000+ double reg_angle, const double prec, const double p, struct rect * rec,2001+ const image_char used, const image_double angles, const double density_th )2002+{2003+ double angle,ang_d,mean_angle,tau,density,xc,yc,ang_c,sum,s_sum;2004+ int i,n;2005+2006+ /* check parameters */2007+ if( reg == NULL ) error("refine: invalid pointer 'reg'.");2008+ if( reg_size == NULL ) error("refine: invalid pointer 'reg_size'.");2009+ if( prec < 0.0 ) error("refine: 'prec' must be positive.");2010+ if( rec == NULL ) error("refine: invalid pointer 'rec'.");2011+ if( used == NULL || used->data == NULL )2012+ error("refine: invalid image 'used'.");2013+ if( angles == NULL || angles->data == NULL )2014+ error("refine: invalid image 'angles'.");2015+2016+ /* compute region points density */2017+ density = (double) *reg_size /2018+ ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width );2019+2020+ /* if the density criterion is satisfied there is nothing to do */2021+ if( density >= density_th ) return TRUE;2022+2023+ /*------ First try: reduce angle tolerance ------*/2024+2025+ /* compute the new mean angle and tolerance */2026+ xc = (double) reg[0].x;2027+ yc = (double) reg[0].y;2028+ ang_c = angles->data[ reg[0].x + reg[0].y * angles->xsize ];2029+ sum = s_sum = 0.0;2030+ n = 0;2031+ for(i=0; i<*reg_size; i++)2032+ {2033+ used->data[ reg[i].x + reg[i].y * used->xsize ] = NOTUSED;2034+ if( dist( xc, yc, (double) reg[i].x, (double) reg[i].y ) < rec->width )2035+ {2036+ angle = angles->data[ reg[i].x + reg[i].y * angles->xsize ];2037+ ang_d = angle_diff_signed(angle,ang_c);2038+ sum += ang_d;2039+ s_sum += ang_d * ang_d;2040+ ++n;2041+ }2042+ }2043+2044+ /* should not happen */2045+ if(n == 0) return FALSE;2046+2047+ mean_angle = sum / (double) n;2048+ tau = 2.0 * sqrt( (s_sum - 2.0 * mean_angle * sum) / (double) n2049+ + mean_angle*mean_angle ); /* 2 * standard deviation */2050+2051+ /* find a new region from the same starting point and new angle tolerance */2052+ region_grow(reg[0].x,reg[0].y,angles,reg,reg_size,&reg_angle,used,tau);2053+2054+ /* if the region is too small, reject */2055+ if( *reg_size < 2 ) return FALSE;2056+2057+ /* re-compute rectangle */2058+ region2rect(reg,*reg_size,modgrad,reg_angle,prec,p,rec);2059+2060+ /* re-compute region points density */2061+ density = (double) *reg_size /2062+ ( dist(rec->x1,rec->y1,rec->x2,rec->y2) * rec->width );2063+2064+ /*------ Second try: reduce region radius ------*/2065+ if( density < density_th )2066+ return reduce_region_radius( reg, reg_size, modgrad, reg_angle, prec, p,2067+ rec, used, angles, density_th );2068+2069+ /* if this point is reached, the density criterion is satisfied */2070+ return TRUE;2071+}2072+2073+2074+/*----------------------------------------------------------------------------*/2075+/*-------------------------- Line Segment Detector ---------------------------*/2076+/*----------------------------------------------------------------------------*/2077+2078+/*----------------------------------------------------------------------------*/2079+/** LSD full interface.2080+ */2081+static2082+double * LineSegmentDetection( int * n_out,2083+ double * img, const int X, const int Y,2084+ const double scale, const double sigma_scale, const double quant,2085+ const double ang_th, const double log_eps, const double density_th,2086+ const int n_bins,2087+ int ** reg_img, int * reg_x, int * reg_y )2088+{2089+ image_double image;2090+ const ntuple_list out = new_ntuple_list(7);2091+ double * return_value;2092+ image_double scaled_image,angles,modgrad;2093+ image_char used;2094+ image_int region = NULL;2095+ struct coorlist * list_p;2096+ void * mem_p;2097+ struct rect rec;2098+ struct point * reg;2099+ int reg_size,min_reg_size,i;2100+ unsigned int xsize,ysize;2101+ double rho,reg_angle,prec,p,log_nfa,logNT;2102+ int ls_count = 0; /* line segments are numbered 1,2,3,... */2103+2104+2105+ /* check parameters */2106+ if( img == NULL || X <= 0 || Y <= 0 ) error("invalid image input.");2107+ if( scale <= 0.0 ) error("'scale' value must be positive.");2108+ if( sigma_scale <= 0.0 ) error("'sigma_scale' value must be positive.");2109+ if( quant < 0.0 ) error("'quant' value must be positive.");2110+ if( ang_th <= 0.0 || ang_th >= 180.0 )2111+ error("'ang_th' value must be in the range (0,180).");2112+ if( density_th < 0.0 || density_th > 1.0 )2113+ error("'density_th' value must be in the range [0,1].");2114+ if( n_bins <= 0 ) error("'n_bins' value must be positive.");2115+2116+2117+ /* angle tolerance */2118+ prec = deg2rad(ang_th);2119+ p = ang_th / 180.0;2120+ rho = quant / sin(prec); /* gradient magnitude threshold */2121+2122+2123+ /* load and scale image (if necessary) and compute angle at each pixel */2124+ image = new_image_double_ptr( (unsigned int) X, (unsigned int) Y, img );2125+ if( scale != 1.0 )2126+ {2127+ scaled_image = gaussian_sampler( image, scale, sigma_scale );2128+ angles = ll_angle( scaled_image, rho, &list_p, &mem_p,2129+ &modgrad, (unsigned int) n_bins );2130+ free_image_double(scaled_image);2131+ }2132+ else2133+ angles = ll_angle( image, rho, &list_p, &mem_p, &modgrad,2134+ (unsigned int) n_bins );2135+ xsize = angles->xsize;2136+ ysize = angles->ysize;2137+2138+ /* Number of Tests - NT2139+2140+ The theoretical number of tests is Np.(XY)^(5/2)2141+ where X and Y are number of columns and rows of the image.2142+ Np corresponds to the number of angle precisions considered.2143+ As the procedure 'rect_improve' tests 5 times to halve the2144+ angle precision, and 5 more times after improving other factors,2145+ 11 different precision values are potentially tested. Thus,2146+ the number of tests is2147+ 11 * (X*Y)^(5/2)2148+ whose logarithm value is2149+ log10(11) + 5/2 * (log10(X) + log10(Y)).2150+ */2151+ logNT = 5.0 * ( log10( (double) xsize ) + log10( (double) ysize ) ) / 2.02152+ + log10(11.0);2153+ min_reg_size = (int) (-logNT/log10(p)); /* minimal number of points in region2154+ that can give a meaningful event */2155+2156+2157+ /* initialize some structures */2158+ if( reg_img != NULL && reg_x != NULL && reg_y != NULL ) /* save region data */2159+ region = new_image_int_ini(angles->xsize,angles->ysize,0);2160+ used = new_image_char_ini(xsize,ysize,NOTUSED);2161+ reg = (struct point *) calloc( (size_t) (xsize*ysize), sizeof(struct point) );2162+ if( reg == NULL ) error("not enough memory!");2163+2164+2165+ /* search for line segments */2166+ for(; list_p != NULL; list_p = list_p->next )2167+ if( used->data[ list_p->x + list_p->y * used->xsize ] == NOTUSED &&2168+ angles->data[ list_p->x + list_p->y * angles->xsize ] != NOTDEF )2169+ /* there is no risk of double comparison problems here2170+ because we are only interested in the exact NOTDEF value */2171+ {2172+ /* find the region of connected point and ~equal angle */2173+ region_grow( list_p->x, list_p->y, angles, reg, &reg_size,2174+ &reg_angle, used, prec );2175+2176+ /* reject small regions */2177+ if( reg_size < min_reg_size ) continue;2178+2179+ /* construct rectangular approximation for the region */2180+ region2rect(reg,reg_size,modgrad,reg_angle,prec,p,&rec);2181+2182+ /* Check if the rectangle exceeds the minimal density of2183+ region points. If not, try to improve the region.2184+ The rectangle will be rejected if the final one does2185+ not fulfill the minimal density condition.2186+ This is an addition to the original LSD algorithm published in2187+ "LSD: A Fast Line Segment Detector with a False Detection Control"2188+ by R. Grompone von Gioi, J. Jakubowicz, J.M. Morel, and G. Randall.2189+ The original algorithm is obtained with density_th = 0.0.2190+ */2191+ if( !refine( reg, &reg_size, modgrad, reg_angle,2192+ prec, p, &rec, used, angles, density_th ) ) continue;2193+2194+ /* compute NFA value */2195+ log_nfa = rect_improve(&rec,angles,logNT,log_eps);2196+ if( log_nfa <= log_eps ) continue;2197+2198+ /* A New Line Segment was found! */2199+ ++ls_count; /* increase line segment counter */2200+2201+ /*2202+ The gradient was computed with a 2x2 mask, its value corresponds to2203+ points with an offset of (0.5,0.5), that should be added to output.2204+ The coordinates origin is at the center of pixel (0,0).2205+ */2206+ rec.x1 += 0.5; rec.y1 += 0.5;2207+ rec.x2 += 0.5; rec.y2 += 0.5;2208+2209+ /* scale the result values if a subsampling was performed */2210+ if( scale != 1.0 )2211+ {2212+ rec.x1 /= scale; rec.y1 /= scale;2213+ rec.x2 /= scale; rec.y2 /= scale;2214+ rec.width /= scale;2215+ }2216+2217+ /* add line segment found to output */2218+ add_7tuple( out, rec.x1, rec.y1, rec.x2, rec.y2,2219+ rec.width, rec.p, log_nfa );2220+2221+ /* add region number to 'region' image if needed */2222+ if( region != NULL )2223+ for(i=0; i<reg_size; i++)2224+ region->data[ reg[i].x + reg[i].y * region->xsize ] = ls_count;2225+ }2226+2227+2228+ /* free memory */2229+ free( (void *) image ); /* only the double_image structure should be freed,2230+ the data pointer was provided to this functions2231+ and should not be destroyed. */2232+ free_image_double(angles);2233+ free_image_double(modgrad);2234+ free_image_char(used);2235+ free( (void *) reg );2236+ free( (void *) mem_p );2237+2238+ /* return the result */2239+ if( reg_img != NULL && reg_x != NULL && reg_y != NULL )2240+ {2241+ if( region == NULL ) error("'region' should be a valid image.");2242+ *reg_img = region->data;2243+ if( region->xsize > (unsigned int) INT_MAX ||2244+ region->ysize > (unsigned int) INT_MAX )2245+ error("region image to big to fit in INT sizes.");2246+ *reg_x = (int) (region->xsize);2247+ *reg_y = (int) (region->ysize);2248+2249+ /* free the 'region' structure.2250+ we cannot use the function 'free_image_int' because we need to keep2251+ the memory with the image data to be returned by this function. */2252+ free( (void *) region );2253+ }2254+ if( out->size > (unsigned int) INT_MAX )2255+ error("too many detections to fit in an INT.");2256+ *n_out = (int) (out->size);2257+2258+ return_value = out->values;2259+ free( (void *) out ); /* only the 'ntuple_list' structure must be freed,2260+ but the 'values' pointer must be keep to return2261+ as a result. */2262+2263+ return return_value;2264+}2265+2266+/*==================================================================================2267+ * end of LSD code2268+ *==================================================================================*/2269+2270+// clang-format on2271+2272+#undef NOTDEF2273+#undef NOTUSED2274+#undef USED2275+#undef RELATIVE_ERROR_FACTOR2276+#undef TABSIZE2277+2278+// clang-format off2279+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py2280+// vim: shiftwidth=2 expandtab tabstop=2 cindent2281+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;2282+// clang-format on2283+
A native/upstream/REVISION +1 -0
A native/upstream/ashift.c +6223 -0
@@ -0,0 +1,6223 @@
1+/*2+ This file is part of darktable,3+ Copyright (C) 2016-2026 darktable developers.4+5+ darktable is free software: you can redistribute it and/or modify6+ it under the terms of the GNU General Public License as published by7+ the Free Software Foundation, either version 3 of the License, or8+ (at your option) any later version.9+10+ darktable is distributed in the hope that it will be useful,11+ but WITHOUT ANY WARRANTY; without even the implied warranty of12+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13+ GNU General Public License for more details.14+15+ You should have received a copy of the GNU General Public License16+ along with darktable. If not, see <http://www.gnu.org/licenses/>.17+*/18+#include "common/gdk_event_utils.h"19+20+#include "bauhaus/bauhaus.h"21+#include "common/bilateral.h"22+#include "common/colorspaces_inline_conversions.h"23+#include "common/debug.h"24+#include "common/imagebuf.h"25+#include "common/interpolation.h"26+#include "common/math.h"27+#include "common/matrices.h"28+#include "common/opencl.h"29+#include "control/control.h"30+#include "develop/develop.h"31+#include "develop/imageop.h"32+#include "develop/imageop_gui.h"33+#include "dtgtk/button.h"34+#include "dtgtk/expander.h"35+#include "gui/accelerators.h"36+#include "gui/draw.h"37+#include "gui/gtk.h"38+#include "iop/iop_api.h"39+#include "gui/guides.h"40+41+#include <assert.h>42+#include <gtk/gtk.h>43+#include <inttypes.h>44+#include <math.h>45+#include <stdlib.h>46+#include <string.h>47+48+// Inspiration to this module comes from the program ShiftN (http://www.shiftn.de) by49+// Marcus Hebel.50+51+// Thanks to Marcus for his support when implementing part of the ShiftN functionality52+// to darktable.53+54+#define ROTATION_RANGE 10 // allowed min/max default range for rotation parameter55+#define ROTATION_RANGE_SOFT 180 // allowed min/max range for rotation parameter with manual adjustment56+#define LENSSHIFT_RANGE 1.0 // allowed min/max default range for lensshift parameters57+#define LENSSHIFT_RANGE_SOFT 2.0 // allowed min/max range for lensshift parameters with manual adjustment58+#define SHEAR_RANGE 0.2 // allowed min/max range for shear parameter59+#define SHEAR_RANGE_SOFT 0.5 // allowed min/max range for shear parameter with manual adjustment60+#define MIN_LINE_LENGTH 5 // the minimum length of a line in pixels to be regarded as relevant61+#define MAX_TANGENTIAL_DEVIATION 30 // by how many degrees a line may deviate from the +/-180 and +/-90 to be regarded as relevant62+#define LSD_SCALE 0.99 // LSD: scaling factor for line detection63+#define LSD_SIGMA_SCALE 0.6 // LSD: sigma for Gaussian filter is computed as sigma = sigma_scale/scale64+#define LSD_QUANT 2.0 // LSD: bound to the quantization error on the gradient norm65+#define LSD_ANG_TH 22.5 // LSD: gradient angle tolerance in degrees66+#define LSD_LOG_EPS 0.0 // LSD: detection threshold: -log10(NFA) > log_eps67+#define LSD_DENSITY_TH 0.7 // LSD: minimal density of region points in rectangle68+#define LSD_N_BINS 1024 // LSD: number of bins in pseudo-ordering of gradient modulus69+#define LSD_GAMMA 0.45 // gamma correction to apply on raw images prior to line detection70+#define RANSAC_RUNS 400 // how many iterations to run in ransac71+#define RANSAC_EPSILON 2 // starting value for ransac epsilon (in -log10 units)72+#define RANSAC_EPSILON_STEP 1 // step size of epsilon optimization (log10 units)73+#define RANSAC_ELIMINATION_RATIO 60 // percentage of lines we try to eliminate as outliers74+#define RANSAC_OPTIMIZATION_STEPS 5 // home many steps to optimize epsilon75+#define RANSAC_OPTIMIZATION_DRY_RUNS 50 // how man runs per optimization steps76+#define RANSAC_HURDLE 5 // hurdle rate: the number of lines below which we do a complete permutation instead of random sampling77+#define MINIMUM_FITLINES 2 // minimum number of lines needed for automatic parameter fit78+#define NMS_EPSILON 1e-3 // break criterion for Nelder-Mead simplex79+#define NMS_SCALE 1.0 // scaling factor for Nelder-Mead simplex80+#define NMS_ITERATIONS 400 // number of iterations for Nelder-Mead simplex81+#define NMS_CROP_EPSILON 100.0 // break criterion for Nelder-Mead simplex on crop fitting82+#define NMS_CROP_SCALE 0.5 // scaling factor for Nelder-Mead simplex on crop fitting83+#define NMS_CROP_ITERATIONS 100 // number of iterations for Nelder-Mead simplex on crop fitting84+#define NMS_ALPHA 1.0 // reflection coefficient for Nelder-Mead simplex85+#define NMS_BETA 0.5 // contraction coefficient for Nelder-Mead simplex86+#define NMS_GAMMA 2.0 // expansion coefficient for Nelder-Mead simplex87+#define DEFAULT_F_LENGTH 28.0 // focal length we assume if no exif data are available88+89+// define to get debugging output90+#undef ASHIFT_DEBUG91+92+// maximum number of drawn lines that can be saved in parameters93+// any change in this value needs to upgrade parameters version !94+#define MAX_SAVED_LINES 5095+96+// For line detection we use the LSD algorithm as published by Rafael Grompone:97+//98+// "LSD: a Line Segment Detector" by Rafael Grompone von Gioi,99+// Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall,100+// Image Processing On Line, 2012. DOI:10.5201/ipol.2012.gjmr-lsd101+// http://dx.doi.org/10.5201/ipol.2012.gjmr-lsd102+#include "ashift_lsd.c"103+104+// For parameter optimization we are using the Nelder-Mead simplex method105+// implemented by Michael F. Hutt.106+#include "ashift_nmsimplex.c"107+108+109+DT_MODULE_INTROSPECTION(5, dt_iop_ashift_params_t)110+111+112+const char *name()113+{114+ return _("rotate and perspective");115+}116+117+const char *aliases()118+{119+ return _("rotation|keystone|distortion|crop|reframe");120+}121+122+const char **description(dt_iop_module_t *self)123+{124+ return dt_iop_set_description(self, _("rotate or distort perspective"),125+ _("corrective or creative"),126+ _("linear, RGB, scene-referred"),127+ _("geometric, RGB"),128+ _("linear, RGB, scene-referred"));129+}130+131+int flags()132+{133+ return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_TILING_FULL_ROI | IOP_FLAGS_ONE_INSTANCE134+ | IOP_FLAGS_ALLOW_FAST_PIPE135+ | IOP_FLAGS_GUIDES_SPECIAL_DRAW | IOP_FLAGS_GUIDES_WIDGET;136+}137+138+int default_group()139+{140+ return IOP_GROUP_CORRECT | IOP_GROUP_TECHNICAL;141+}142+143+int operation_tags()144+{145+ return IOP_TAG_DISTORT;146+}147+148+int operation_tags_filter()149+{150+ // switch off cropping and decoration, we want to see the full image.151+ return IOP_TAG_DECORATION | IOP_TAG_CROPPING;152+}153+154+dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,155+ dt_dev_pixelpipe_t *pipe,156+ dt_dev_pixelpipe_iop_t *piece)157+{158+ return IOP_CS_RGB;159+}160+161+typedef enum dt_iop_ashift_method_t162+{163+ ASHIFT_METHOD_NONE = 0,164+ ASHIFT_METHOD_AUTO,165+ ASHIFT_METHOD_QUAD,166+ ASHIFT_METHOD_LINES167+} dt_iop_ashift_method_t;168+169+typedef enum dt_iop_ashift_homodir_t170+{171+ ASHIFT_HOMOGRAPH_FORWARD,172+ ASHIFT_HOMOGRAPH_INVERTED173+} dt_iop_ashift_homodir_t;174+175+typedef enum dt_iop_ashift_linetype_t176+{177+ ASHIFT_LINE_IRRELEVANT = 0, // the line is found to be not interesting178+ // eg. too short, or not horizontal or vertical179+ ASHIFT_LINE_RELEVANT = 1 << 0, // the line is relevant for us180+ ASHIFT_LINE_DIRVERT = 1 << 1, // the line is (mostly) vertical, else (mostly) horizontal181+ ASHIFT_LINE_SELECTED = 1 << 2, // the line is selected for fitting182+ ASHIFT_LINE_VERTICAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT,183+ ASHIFT_LINE_HORIZONTAL_NOT_SELECTED = ASHIFT_LINE_RELEVANT,184+ ASHIFT_LINE_VERTICAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED,185+ ASHIFT_LINE_HORIZONTAL_SELECTED = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED,186+ ASHIFT_LINE_MASK = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_DIRVERT | ASHIFT_LINE_SELECTED187+} dt_iop_ashift_linetype_t;188+189+typedef enum dt_iop_ashift_linecolor_t190+{191+ ASHIFT_LINECOLOR_GREY = 0,192+ ASHIFT_LINECOLOR_GREEN = 1,193+ ASHIFT_LINECOLOR_RED = 2,194+ ASHIFT_LINECOLOR_BLUE = 3,195+ ASHIFT_LINECOLOR_YELLOW = 4196+} dt_iop_ashift_linecolor_t;197+198+typedef enum dt_iop_ashift_fitaxis_t199+{200+ ASHIFT_FIT_NONE = 0, // none201+ ASHIFT_FIT_ROTATION = 1 << 0, // flag indicates to fit rotation angle202+ ASHIFT_FIT_LENS_VERT = 1 << 1, // flag indicates to fit vertical lens shift203+ ASHIFT_FIT_LENS_HOR = 1 << 2, // flag indicates to fit horizontal lens shift204+ ASHIFT_FIT_SHEAR = 1 << 3, // flag indicates to fit shear parameter205+ ASHIFT_FIT_LINES_VERT = 1 << 4, // use vertical lines for fitting206+ ASHIFT_FIT_LINES_HOR = 1 << 5, // use horizontal lines for fitting207+ ASHIFT_FIT_LENS_BOTH = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR,208+ ASHIFT_FIT_LINES_BOTH = ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,209+ ASHIFT_FIT_VERTICALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT,210+ ASHIFT_FIT_HORIZONTALLY = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR,211+ ASHIFT_FIT_BOTH = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |212+ ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,213+ ASHIFT_FIT_VERTICALLY_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LINES_VERT,214+ ASHIFT_FIT_HORIZONTALLY_NO_ROTATION = ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_HOR,215+ ASHIFT_FIT_BOTH_NO_ROTATION = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |216+ ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,217+ ASHIFT_FIT_BOTH_SHEAR = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR |218+ ASHIFT_FIT_SHEAR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,219+ ASHIFT_FIT_ROTATION_VERTICAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT,220+ ASHIFT_FIT_ROTATION_HORIZONTAL_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_HOR,221+ ASHIFT_FIT_ROTATION_BOTH_LINES = ASHIFT_FIT_ROTATION | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR,222+ ASHIFT_FIT_FLIP = ASHIFT_FIT_LENS_VERT | ASHIFT_FIT_LENS_HOR | ASHIFT_FIT_LINES_VERT | ASHIFT_FIT_LINES_HOR223+} dt_iop_ashift_fitaxis_t;224+225+typedef enum dt_iop_ashift_nmsresult_t226+{227+ NMS_SUCCESS = 0,228+ NMS_NOT_ENOUGH_LINES = 1,229+ NMS_DID_NOT_CONVERGE = 2,230+ NMS_INSANE = 3231+} dt_iop_ashift_nmsresult_t;232+233+typedef enum dt_iop_ashift_enhance_t234+{235+ ASHIFT_ENHANCE_NONE = 0,236+ ASHIFT_ENHANCE_EDGES = 1 << 0,237+ ASHIFT_ENHANCE_DETAIL = 1 << 1,238+ ASHIFT_ENHANCE_HORIZONTAL = 0x100,239+ ASHIFT_ENHANCE_VERTICAL = 0x200240+} dt_iop_ashift_enhance_t;241+242+typedef enum dt_iop_ashift_mode_t243+{244+ ASHIFT_MODE_GENERIC = 0, // $DESCRIPTION: "generic"245+ ASHIFT_MODE_SPECIFIC = 1 // $DESCRIPTION: "specific"246+} dt_iop_ashift_mode_t;247+248+typedef enum dt_iop_ashift_crop_t249+{250+ ASHIFT_CROP_OFF = 0, // $DESCRIPTION: "off"251+ ASHIFT_CROP_LARGEST = 1,// $DESCRIPTION: "largest area"252+ ASHIFT_CROP_ASPECT = 2 // $DESCRIPTION: "original format"253+} dt_iop_ashift_crop_t;254+255+typedef enum dt_iop_ashift_bounding_t256+{257+ ASHIFT_BOUNDING_OFF = 0,258+ ASHIFT_BOUNDING_SELECT = 1,259+ ASHIFT_BOUNDING_DESELECT = 2260+} dt_iop_ashift_bounding_t;261+262+typedef enum dt_iop_ashift_jobcode_t263+{264+ ASHIFT_JOBCODE_NONE = 0,265+ ASHIFT_JOBCODE_GET_STRUCTURE = 1,266+ ASHIFT_JOBCODE_FIT = 2,267+ ASHIFT_JOBCODE_GET_STRUCTURE_LINES = 3,268+ ASHIFT_JOBCODE_GET_STRUCTURE_QUAD = 4,269+ ASHIFT_JOBCODE_DO_CROP = 5270+} dt_iop_ashift_jobcode_t;271+272+typedef struct dt_iop_ashift_params_t273+{274+ float rotation; // $MIN: -ROTATION_RANGE_SOFT $MAX: ROTATION_RANGE_SOFT $DEFAULT: 0.0275+ float lensshift_v; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (vertical)"276+ float lensshift_h; // $MIN: -LENSSHIFT_RANGE_SOFT $MAX: LENSSHIFT_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "lens shift (horizontal)"277+ float shear; // $MIN: -SHEAR_RANGE_SOFT $MAX: SHEAR_RANGE_SOFT $DEFAULT: 0.0 $DESCRIPTION: "shear"278+ float f_length; // $MIN: 1.0 $MAX: 2000.0 $DEFAULT: DEFAULT_F_LENGTH $DESCRIPTION: "focal length"279+ float crop_factor; // $MIN: 0.5 $MAX: 10.0 $DEFAULT: 1.0 $DESCRIPTION: "crop factor"280+ float orthocorr; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "lens dependence"281+ float aspect; // $MIN: 0.5 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "aspect adjust"282+ dt_iop_ashift_mode_t mode; // $DEFAULT: ASHIFT_MODE_GENERIC $DESCRIPTION: "lens model"283+ dt_iop_ashift_crop_t cropmode; // $DEFAULT: ASHIFT_CROP_LARGEST $DESCRIPTION: "automatic cropping"284+ float cl; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0285+ float cr; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0286+ float ct; // $DEFAULT: 0.0 $MIN: 0.0 $MAX: 1.0287+ float cb; // $DEFAULT: 1.0 $MIN: 0.0 $MAX: 1.0288+ float last_drawn_lines[MAX_SAVED_LINES * 4];289+ int last_drawn_lines_count;290+ float last_quad_lines[8];291+} dt_iop_ashift_params_t;292+293+typedef struct dt_iop_ashift_line_t294+{295+ float p1[3];296+ float p2[3];297+ float length;298+ float width;299+ float weight;300+ dt_iop_ashift_linetype_t type;301+ // homogeneous coordinates:302+ float DT_ALIGNED_PIXEL L[3];303+} dt_iop_ashift_line_t;304+305+typedef struct dt_iop_ashift_points_idx_t306+{307+ size_t offset;308+ int length;309+ int near;310+ int bounded;311+ dt_iop_ashift_linetype_t type;312+ dt_iop_ashift_linecolor_t color;313+ // bounding box:314+ float bbx, bby, bbX, bbY;315+} dt_iop_ashift_points_idx_t;316+317+typedef struct dt_iop_ashift_fit_params_t318+{319+ int params_count;320+ dt_iop_ashift_linetype_t linetype;321+ dt_iop_ashift_linetype_t linemask;322+ dt_iop_ashift_line_t *lines;323+ int lines_count;324+ int width;325+ int height;326+ float weight;327+ float f_length_kb;328+ float orthocorr;329+ float aspect;330+ float rotation;331+ float lensshift_v;332+ float lensshift_h;333+ float shear;334+ float rotation_range;335+ float lensshift_v_range;336+ float lensshift_h_range;337+ float shear_range;338+} dt_iop_ashift_fit_params_t;339+340+typedef struct dt_iop_ashift_cropfit_params_t341+{342+ int width;343+ int height;344+ float x;345+ float y;346+ float alpha;347+ float DT_ALIGNED_ARRAY homograph[3][3];348+ float DT_ALIGNED_ARRAY edges[4][3];349+} dt_iop_ashift_cropfit_params_t;350+351+typedef struct dt_iop_ashift_gui_data_t352+{353+ GtkWidget *rotation;354+ GtkWidget *lensshift_v;355+ GtkWidget *lensshift_h;356+ GtkWidget *shear;357+ GtkWidget *cropmode;358+ GtkWidget *mode;359+ GtkWidget *specifics;360+ GtkWidget *f_length;361+ GtkWidget *crop_factor;362+ GtkWidget *orthocorr;363+ GtkWidget *aspect;364+ GtkWidget *fit_v;365+ GtkWidget *fit_h;366+ GtkWidget *fit_both;367+ GtkWidget *structure_auto;368+ GtkWidget *structure_quad;369+ GtkWidget *structure_lines;370+ gboolean straightening;371+ float straighten_x;372+ float straighten_y;373+ int fitting;374+ int isflipped;375+ int isselecting;376+ int isdeselecting;377+ dt_iop_ashift_bounding_t isbounding;378+ float near_delta;379+ int selecting_lines_version;380+ float rotation_range;381+ float lensshift_v_range;382+ float lensshift_h_range;383+ float shear_range;384+ dt_iop_ashift_line_t *lines;385+ int lines_in_width;386+ int lines_in_height;387+ int lines_x_off;388+ int lines_y_off;389+ int lines_count;390+ int vertical_count;391+ int horizontal_count;392+ int lines_version;393+ float vertical_weight;394+ float horizontal_weight;395+ float *points;396+ dt_iop_ashift_points_idx_t *points_idx;397+ int points_lines_count;398+ int points_version;399+ float *buf;400+ int buf_width;401+ int buf_height;402+ int buf_x_off;403+ int buf_y_off;404+ float buf_scale;405+ dt_hash_t lines_hash;406+ dt_hash_t grid_hash;407+ dt_hash_t buf_hash;408+ dt_iop_ashift_fitaxis_t lastfit;409+ float lastx;410+ float lasty;411+ float crop_cx;412+ float crop_cy;413+ dt_iop_ashift_jobcode_t jobcode;414+ int jobparams;415+ gboolean adjust_crop;416+ float cl; // shadow copy of dt_iop_ashift_data_t.cl417+ float cr; // shadow copy of dt_iop_ashift_data_t.cr418+ float ct; // shadow copy of dt_iop_ashift_data_t.ct419+ float cb; // shadow copy of dt_iop_ashift_data_t.cb420+421+ dt_iop_ashift_method_t current_structure_method;422+ int draw_near_point;423+ gboolean draw_point_move;424+ int draw_line_move;425+ float draw_pointmove_x;426+ float draw_pointmove_y;427+ float *draw_points;428+ dt_gui_collapsible_section_t cs;429+} dt_iop_ashift_gui_data_t;430+431+typedef struct dt_iop_ashift_data_t432+{433+ float rotation;434+ float lensshift_v;435+ float lensshift_h;436+ float shear;437+ float f_length_kb;438+ float orthocorr;439+ float aspect;440+ float cl;441+ float cr;442+ float ct;443+ float cb;444+} dt_iop_ashift_data_t;445+446+typedef struct dt_iop_ashift_global_data_t447+{448+ int kernel_ashift_bilinear;449+ int kernel_ashift_bicubic;450+ int kernel_ashift_lanczos2;451+ int kernel_ashift_lanczos3;452+} dt_iop_ashift_global_data_t;453+454+int legacy_params(dt_iop_module_t *self,455+ const void *const old_params,456+ const int old_version,457+ void **new_params,458+ int32_t *new_params_size,459+ int *new_version)460+{461+ typedef struct dt_iop_ashift_params_v5_t462+ {463+ float rotation;464+ float lensshift_v;465+ float lensshift_h;466+ float shear;467+ float f_length;468+ float crop_factor;469+ float orthocorr;470+ float aspect;471+ dt_iop_ashift_mode_t mode;472+ dt_iop_ashift_crop_t cropmode;473+ float cl;474+ float cr;475+ float ct;476+ float cb;477+ float last_drawn_lines[MAX_SAVED_LINES * 4];478+ int last_drawn_lines_count;479+ float last_quad_lines[8];480+ } dt_iop_ashift_params_v5_t;481+482+ if(old_version == 1)483+ {484+ typedef struct dt_iop_ashift_params_v1_t485+ {486+ float rotation;487+ float lensshift_v;488+ float lensshift_h;489+ int toggle;490+ } dt_iop_ashift_params_v1_t;491+492+ const dt_iop_ashift_params_v1_t *o = old_params;493+ dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));494+ n->rotation = o->rotation;495+ n->lensshift_v = o->lensshift_v;496+ n->lensshift_h = o->lensshift_h;497+ n->shear = 0.0f;498+ n->f_length = DEFAULT_F_LENGTH;499+ n->crop_factor = 1.0f;500+ n->orthocorr = 100.0f;501+ n->aspect = 1.0f;502+ n->mode = ASHIFT_MODE_GENERIC;503+ n->cropmode = ASHIFT_CROP_OFF;504+ n->cl = 0.0f;505+ n->cr = 1.0f;506+ n->ct = 0.0f;507+ n->cb = 1.0f;508+ for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;509+ for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;510+ n->last_drawn_lines_count = 0;511+512+ *new_params = n;513+ *new_params_size = sizeof(dt_iop_ashift_params_v5_t);514+ *new_version = 5;515+ return 0;516+ }517+ if(old_version == 2)518+ {519+ typedef struct dt_iop_ashift_params_v2_t520+ {521+ float rotation;522+ float lensshift_v;523+ float lensshift_h;524+ float f_length;525+ float crop_factor;526+ float orthocorr;527+ float aspect;528+ dt_iop_ashift_mode_t mode;529+ int toggle;530+ } dt_iop_ashift_params_v2_t;531+532+ const dt_iop_ashift_params_v2_t *o = old_params;533+ dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));534+ n->rotation = o->rotation;535+ n->lensshift_v = o->lensshift_v;536+ n->lensshift_h = o->lensshift_h;537+ n->shear = 0.0f;538+ n->f_length = o->f_length;539+ n->crop_factor = o->crop_factor;540+ n->orthocorr = o->orthocorr;541+ n->aspect = o->aspect;542+ n->mode = o->mode;543+ n->cropmode = ASHIFT_CROP_OFF;544+ n->cl = 0.0f;545+ n->cr = 1.0f;546+ n->ct = 0.0f;547+ n->cb = 1.0f;548+ for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;549+ for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;550+ n->last_drawn_lines_count = 0;551+552+ *new_params = n;553+ *new_params_size = sizeof(dt_iop_ashift_params_v5_t);554+ *new_version = 5;555+ return 0;556+ }557+ if(old_version == 3)558+ {559+ typedef struct dt_iop_ashift_params_v3_t560+ {561+ float rotation;562+ float lensshift_v;563+ float lensshift_h;564+ float f_length;565+ float crop_factor;566+ float orthocorr;567+ float aspect;568+ dt_iop_ashift_mode_t mode;569+ int toggle;570+ dt_iop_ashift_crop_t cropmode;571+ float cl;572+ float cr;573+ float ct;574+ float cb;575+ } dt_iop_ashift_params_v3_t;576+577+ const dt_iop_ashift_params_v3_t *o = old_params;578+ dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));579+ n->rotation = o->rotation;580+ n->lensshift_v = o->lensshift_v;581+ n->lensshift_h = o->lensshift_h;582+ n->shear = 0.0f;583+ n->f_length = o->f_length;584+ n->crop_factor = o->crop_factor;585+ n->orthocorr = o->orthocorr;586+ n->aspect = o->aspect;587+ n->mode = o->mode;588+ n->cropmode = o->cropmode;589+ n->cl = o->cl;590+ n->cr = o->cr;591+ n->ct = o->ct;592+ n->cb = o->cb;593+ for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;594+ for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;595+ n->last_drawn_lines_count = 0;596+597+ *new_params = n;598+ *new_params_size = sizeof(dt_iop_ashift_params_v5_t);599+ *new_version = 5;600+ return 0;601+ }602+ if(old_version == 4)603+ {604+ typedef struct dt_iop_ashift_params_v4_t605+ {606+ float rotation;607+ float lensshift_v;608+ float lensshift_h;609+ float shear;610+ float f_length;611+ float crop_factor;612+ float orthocorr;613+ float aspect;614+ dt_iop_ashift_mode_t mode;615+ int toggle;616+ dt_iop_ashift_crop_t cropmode;617+ float cl;618+ float cr;619+ float ct;620+ float cb;621+ } dt_iop_ashift_params_v4_t;622+623+ const dt_iop_ashift_params_v4_t *o = old_params;624+ dt_iop_ashift_params_v5_t *n = malloc(sizeof(dt_iop_ashift_params_v5_t));625+ n->rotation = o->rotation;626+ n->lensshift_v = o->lensshift_v;627+ n->lensshift_h = o->lensshift_h;628+ n->shear = o->shear;629+ n->f_length = o->f_length;630+ n->crop_factor = o->crop_factor;631+ n->orthocorr = o->orthocorr;632+ n->aspect = o->aspect;633+ n->mode = o->mode;634+ n->cropmode = o->cropmode;635+ n->cl = o->cl;636+ n->cr = o->cr;637+ n->ct = o->ct;638+ n->cb = o->cb;639+ for(int i = 0; i < MAX_SAVED_LINES * 4; i++) n->last_drawn_lines[i] = 0.0f;640+ for(int i = 0; i < 8; i++) n->last_quad_lines[i] = 0.0f;641+ n->last_drawn_lines_count = 0;642+643+ *new_params = n;644+ *new_params_size = sizeof(dt_iop_ashift_params_v5_t);645+ *new_version = 5;646+ return 0;647+ }648+649+ return 1;650+}651+652+// normalized product of two 3x1 vectors653+// dst needs to be different from v1 and v2654+static inline void vec3prodn(float *dst, const float *const v1, const float *const v2)655+{656+ const float l1 = v1[1] * v2[2] - v1[2] * v2[1];657+ const float l2 = v1[2] * v2[0] - v1[0] * v2[2];658+ const float l3 = v1[0] * v2[1] - v1[1] * v2[0];659+660+ // normalize so that l1^2 + l2^2 + l3^3 = 1661+ const float sq = sqrtf(l1 * l1 + l2 * l2 + l3 * l3);662+663+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;664+665+ dst[0] = l1 * f;666+ dst[1] = l2 * f;667+ dst[2] = l3 * f;668+}669+670+// normalize a 3x1 vector so that x^2 + y^2 + z^2 = 1671+// dst and v may be the same672+static inline void vec3norm(float *dst, const float *const v)673+{674+ const float sq = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);675+676+ // special handling for an all-zero vector677+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;678+679+ dst[0] = v[0] * f;680+ dst[1] = v[1] * f;681+ dst[2] = v[2] * f;682+}683+684+// normalize a 3x1 vector so that x^2 + y^2 = 1; a useful normalization for685+// lines in homogeneous coordinates686+// dst and v may be the same687+static inline void vec3lnorm(float *dst, const float *const v)688+{689+ const float sq = dt_fast_hypotf(v[0], v[1]);690+691+ // special handling for a point vector of the image center692+ const float f = sq > 0.0f ? 1.0f / sq : 1.0f;693+694+ dst[0] = v[0] * f;695+ dst[1] = v[1] * f;696+ dst[2] = v[2] * f;697+}698+699+700+// scalar product of two 3x1 vectors701+static inline float vec3scalar(const float *const v1, const float *const v2)702+{703+ return (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]);704+}705+706+// check if 3x1 vector is (very close to) null707+static inline int vec3isnull(const float *const v)708+{709+ const float eps = 1e-10f;710+ return (fabsf(v[0]) < eps && fabsf(v[1]) < eps && fabsf(v[2]) < eps);711+}712+713+static inline void _shadow_crop_box(const dt_iop_ashift_params_t *p,714+ dt_iop_ashift_gui_data_t *g)715+{716+ // copy actual crop box values into shadow variables717+ g->cl = p->cl;718+ g->cr = p->cr;719+ g->ct = p->ct;720+ g->cb = p->cb;721+}722+723+static void _clear_shadow_crop_box(dt_iop_ashift_gui_data_t *g)724+{725+ // reset the crop to the full image726+ g->cl = 0.0f;727+ g->cr = 1.0f;728+ g->ct = 0.0f;729+ g->cb = 1.0f;730+}731+732+static inline void _commit_crop_box(dt_iop_ashift_params_t *p,733+ const dt_iop_ashift_gui_data_t *g)734+{735+ // copy shadow values for crop box into actual parameters736+ p->cl = g->cl;737+ p->cr = g->cr;738+ p->ct = g->ct;739+ p->cb = g->cb;740+}741+742+static inline void _swap_shadow_crop_box(dt_iop_ashift_params_t *p,743+ dt_iop_ashift_gui_data_t *g)744+{745+ // exchange shadow values and actual crop values this is needed for746+ // a temporary commit to be able to properly update the undo history747+ float tmp;748+ tmp = p->cl; p->cl = g->cl; g->cl = tmp;749+ tmp = p->cr; p->cr = g->cr; g->cr = tmp;750+ tmp = p->ct; p->ct = g->ct; g->ct = tmp;751+ tmp = p->cb; p->cb = g->cb; g->cb = tmp;752+}753+754+#define MAT3SWAP(a, b) { float (*tmp)[3] = (a); (a) = (b); (b) = tmp; }755+756+static void _homography(float *homograph,757+ const float angle,758+ const float shift_v,759+ const float shift_h,760+ const float shear,761+ const float f_length_kb,762+ const float orthocorr,763+ const float aspect,764+ const int width,765+ const int height,766+ const dt_iop_ashift_homodir_t dir)767+{768+ // calculate homograph that combines all translations, rotations769+ // and warping into one single matrix operation.770+ // this is heavily leaning on ShiftN where the homographic matrix expects771+ // input in (y : x : 1) format. in the darktable world we want to keep the772+ // (x : y : 1) convention. therefore we need to flip coordinates first and773+ // make sure that output is in correct format after corrections are applied.774+775+ const float u = width;776+ const float v = height;777+778+ const float phi = deg2radf(angle);779+ const float cosi = cosf(phi);780+ const float sini = sinf(phi);781+ const float ascale = sqrtf(aspect);782+783+ // most of this comes from ShiftN784+ const float f_global = f_length_kb;785+ const float horifac = 1.0f - orthocorr / 100.0f;786+ const float exppa_v = expf(shift_v);787+ const float fdb_v = f_global / (14.4f + (v / u - 1) * 7.2f);788+ const float rad_v = fdb_v * (exppa_v - 1.0f) / (exppa_v + 1.0f);789+ const float alpha_v = CLAMP(atanf(rad_v), -1.5f, 1.5f);790+ const float rt_v = sinf(0.5f * alpha_v);791+ const float r_v = fmaxf(0.1f, 2.0f * (horifac - 1.0f) * rt_v * rt_v + 1.0f);792+793+ const float vertifac = 1.0f - orthocorr / 100.0f;794+ const float exppa_h = expf(shift_h);795+ const float fdb_h = f_global / (14.4f + (u / v - 1) * 7.2f);796+ const float rad_h = fdb_h * (exppa_h - 1.0f) / (exppa_h + 1.0f);797+ const float alpha_h = CLAMP(atanf(rad_h), -1.5f, 1.5f);798+ const float rt_h = sinf(0.5f * alpha_h);799+ const float r_h = fmaxf(0.1f, 2.0f * (vertifac - 1.0f) * rt_h * rt_h + 1.0f);800+801+802+ // three intermediate buffers for matrix calculation ...803+ float DT_ALIGNED_ARRAY m1[3][3];804+ float DT_ALIGNED_ARRAY m2[3][3];805+ float DT_ALIGNED_ARRAY m3[3][3];806+807+ // ... and some pointers to handle them more intuitively808+ float (*mwork)[3] = m1;809+ float (*minput)[3] = m2;810+ float (*moutput)[3] = m3;811+812+ // Step 1: flip x and y coordinates (see above)813+ memset(minput, 0, sizeof(float) * 9);814+ minput[0][1] = 1.0f;815+ minput[1][0] = 1.0f;816+ minput[2][2] = 1.0f;817+818+819+ // Step 2: rotation of image around its center820+ memset(mwork, 0, sizeof(float) * 9);821+ mwork[0][0] = cosi;822+ mwork[0][1] = -sini;823+ mwork[1][0] = sini;824+ mwork[1][1] = cosi;825+ mwork[0][2] = -0.5f * v * cosi + 0.5f * u * sini + 0.5f * v;826+ mwork[1][2] = -0.5f * v * sini - 0.5f * u * cosi + 0.5f * u;827+ mwork[2][2] = 1.0f;828+829+ // multiply mwork * minput -> moutput830+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);831+832+833+ // Step 3: apply shearing834+ memset(mwork, 0, sizeof(float) * 9);835+ mwork[0][0] = 1.0f;836+ mwork[0][1] = shear;837+ mwork[1][1] = 1.0f;838+ mwork[1][0] = shear;839+ mwork[2][2] = 1.0f;840+841+ // moutput (of last calculation) -> minput842+ MAT3SWAP(minput, moutput);843+ // multiply mwork * minput -> moutput844+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);845+846+847+ // Step 4: apply vertical lens shift effect848+ memset(mwork, 0, sizeof(float) * 9);849+ mwork[0][0] = exppa_v;850+ mwork[1][0] = 0.5f * ((exppa_v - 1.0f) * u) / v;851+ mwork[1][1] = 2.0f * exppa_v / (exppa_v + 1.0f);852+ mwork[1][2] = -0.5f * ((exppa_v - 1.0f) * u) / (exppa_v + 1.0f);853+ mwork[2][0] = (exppa_v - 1.0f) / v;854+ mwork[2][2] = 1.0f;855+856+ // moutput (of last calculation) -> minput857+ MAT3SWAP(minput, moutput);858+ // multiply mwork * minput -> moutput859+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);860+861+862+ // Step 5: horizontal compression863+ memset(mwork, 0, sizeof(float) * 9);864+ mwork[0][0] = 1.0f;865+ mwork[1][1] = r_v;866+ mwork[1][2] = 0.5f * u * (1.0f - r_v);867+ mwork[2][2] = 1.0f;868+869+ // moutput (of last calculation) -> minput870+ MAT3SWAP(minput, moutput);871+ // multiply mwork * minput -> moutput872+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);873+874+875+ // Step 6: flip x and y back again876+ memset(mwork, 0, sizeof(float) * 9);877+ mwork[0][1] = 1.0f;878+ mwork[1][0] = 1.0f;879+ mwork[2][2] = 1.0f;880+881+ // moutput (of last calculation) -> minput882+ MAT3SWAP(minput, moutput);883+ // multiply mwork * minput -> moutput884+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);885+886+887+ // from here output vectors would be in (x : y : 1) format888+889+ // Step 7: now we can apply horizontal lens shift with the same matrix format as above890+ memset(mwork, 0, sizeof(float) * 9);891+ mwork[0][0] = exppa_h;892+ mwork[1][0] = 0.5f * ((exppa_h - 1.0f) * v) / u;893+ mwork[1][1] = 2.0f * exppa_h / (exppa_h + 1.0f);894+ mwork[1][2] = -0.5f * ((exppa_h - 1.0f) * v) / (exppa_h + 1.0f);895+ mwork[2][0] = (exppa_h - 1.0f) / u;896+ mwork[2][2] = 1.0f;897+898+ // moutput (of last calculation) -> minput899+ MAT3SWAP(minput, moutput);900+ // multiply mwork * minput -> moutput901+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);902+903+904+ // Step 8: vertical compression905+ memset(mwork, 0, sizeof(float) * 9);906+ mwork[0][0] = 1.0f;907+ mwork[1][1] = r_h;908+ mwork[1][2] = 0.5f * v * (1.0f - r_h);909+ mwork[2][2] = 1.0f;910+911+ // moutput (of last calculation) -> minput912+ MAT3SWAP(minput, moutput);913+ // multiply mwork * minput -> moutput914+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);915+916+917+ // Step 9: apply aspect ratio scaling918+ memset(mwork, 0, sizeof(float) * 9);919+ mwork[0][0] = 1.0f * ascale;920+ mwork[1][1] = 1.0f / ascale;921+ mwork[2][2] = 1.0f;922+923+ // moutput (of last calculation) -> minput924+ MAT3SWAP(minput, moutput);925+ // multiply mwork * minput -> moutput926+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);927+928+929+ // Step 10: find x/y offsets and apply according correction so that930+ // no negative coordinates occur in output vector931+ float umin = FLT_MAX, vmin = FLT_MAX;932+ // visit all four corners933+ for(int y = 0; y < height; y += height - 1)934+ for(int x = 0; x < width; x += width - 1)935+ {936+ float pi[3], po[3];937+ pi[0] = x;938+ pi[1] = y;939+ pi[2] = 1.0f;940+ // moutput expects input in (x:y:1) format and gives output as (x:y:1)941+ mat3mulv(po, (float *)moutput, pi);942+ umin = MIN(umin, po[0] / po[2]);943+ vmin = MIN(vmin, po[1] / po[2]);944+ }945+946+ memset(mwork, 0, sizeof(float) * 9);947+ mwork[0][0] = 1.0f;948+ mwork[1][1] = 1.0f;949+ mwork[2][2] = 1.0f;950+ mwork[0][2] = -umin;951+ mwork[1][2] = -vmin;952+953+ // moutput (of last calculation) -> minput954+ MAT3SWAP(minput, moutput);955+ // multiply mwork * minput -> moutput956+ mat3mul((float *)moutput, (float *)mwork, (float *)minput);957+958+959+ // on request we either keep the final matrix for forward conversions960+ // or produce an inverted matrix for backward conversions961+ if(dir == ASHIFT_HOMOGRAPH_FORWARD)962+ {963+ // we have what we need -> copy it to the right place964+ memcpy(homograph, moutput, sizeof(float) * 9);965+ }966+ else967+ {968+ // generate inverted homograph (mat3inv function defined in colorspaces.c)969+ if(mat3inv((float *)homograph, (float *)moutput))970+ {971+ // in case of error we set to unity matrix972+ memset(mwork, 0, sizeof(float) * 9);973+ mwork[0][0] = 1.0f;974+ mwork[1][1] = 1.0f;975+ mwork[2][2] = 1.0f;976+ memcpy(homograph, mwork, sizeof(float) * 9);977+ }978+ }979+}980+#undef MAT3SWAP981+982+983+// check if module parameters are set to all neutral values in which case the module's984+// output is identical to its input985+static inline gboolean _isneutral(const dt_iop_ashift_data_t *data)986+{987+ // values lower than this have no visible effect988+ const float eps = 1.0e-4f;989+990+ return(feqf(data->rotation, 0.0f, eps) &&991+ feqf(data->lensshift_v, 0.0f, eps) &&992+ feqf(data->lensshift_h, 0.0f, eps) &&993+ feqf(data->shear, 0, eps) &&994+ feqf(data->aspect, 1.0f, eps) &&995+ feqf(data->cl, 0.0f, eps) &&996+ feqf(data->cr, 1.0f, eps) &&997+ feqf(data->ct, 0.0f, eps) &&998+ feqf(data->cb, 1.0f, eps));999+}1000+1001+1002+gboolean distort_transform(dt_iop_module_t *self,1003+ dt_dev_pixelpipe_iop_t *piece,1004+ float *const restrict points,1005+ const size_t points_count)1006+{1007+ const dt_iop_ashift_data_t *const data = piece->data;1008+1009+ // nothing to be done if parameters are set to neutral values1010+ if(_isneutral(data)) return TRUE;1011+1012+ float DT_ALIGNED_ARRAY homograph[3][3];1013+ _homography((float *)homograph, data->rotation, data->lensshift_v, data->lensshift_h,1014+ data->shear, data->f_length_kb,1015+ data->orthocorr, data->aspect,1016+ piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_FORWARD);1017+1018+ // clipping offset1019+ const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl);1020+ const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct);1021+ const float cx = fullwidth * data->cl;1022+ const float cy = fullheight * data->ct;1023+1024+ float *const pts = DT_IS_ALIGNED(points);1025+1026+ DT_OMP_FOR(if(points_count > 100))1027+ for(size_t i = 0; i < points_count * 2; i += 2)1028+ {1029+ const float DT_ALIGNED_PIXEL pi[3] = { pts[i], pts[i + 1], 1.0f };1030+ float DT_ALIGNED_PIXEL po[3];1031+ mat3mulv(po, (float *)homograph, pi);1032+ pts[i] = po[0] / po[2] - cx;1033+ pts[i + 1] = po[1] / po[2] - cy;1034+ }1035+1036+ return TRUE;1037+}1038+1039+1040+gboolean distort_backtransform(dt_iop_module_t *self,1041+ dt_dev_pixelpipe_iop_t *piece,1042+ float *points,1043+ const size_t points_count)1044+{1045+ const dt_iop_ashift_data_t *const data = piece->data;1046+1047+ // nothing to be done if parameters are set to neutral values1048+ if(_isneutral(data)) return TRUE;1049+1050+ float DT_ALIGNED_ARRAY ihomograph[3][3];1051+ _homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h,1052+ data->shear, data->f_length_kb,1053+ data->orthocorr, data->aspect,1054+ piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED);1055+1056+ // clipping offset1057+ const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl);1058+ const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct);1059+ const float cx = fullwidth * data->cl;1060+ const float cy = fullheight * data->ct;1061+1062+ float *const pts = DT_IS_ALIGNED(points);1063+1064+ DT_OMP_FOR(if(points_count > 100))1065+ for(size_t i = 0; i < points_count * 2; i += 2)1066+ {1067+ const float DT_ALIGNED_PIXEL pi[3] = { pts[i] + cx, pts[i + 1] + cy, 1.0f };1068+ float DT_ALIGNED_PIXEL po[3];1069+ mat3mulv(po, (float *)ihomograph, pi);1070+ pts[i] = po[0] / po[2];1071+ pts[i + 1] = po[1] / po[2];1072+ }1073+1074+ return TRUE;1075+}1076+1077+void distort_mask(dt_iop_module_t *self,1078+ dt_dev_pixelpipe_iop_t *piece,1079+ const float *const in,1080+ float *const out,1081+ const dt_iop_roi_t *const roi_in,1082+ const dt_iop_roi_t *const roi_out)1083+{1084+ const dt_iop_ashift_data_t *const data = piece->data;1085+1086+ // if module is set to neutral parameters we just copy input->output and are done1087+ if(_isneutral(data))1088+ {1089+ dt_iop_image_copy_by_size(out, in, roi_out->width, roi_out->height, 1);1090+ return;1091+ }1092+1093+ const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);1094+1095+ float DT_ALIGNED_ARRAY ihomograph[3][3];1096+ _homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h,1097+ data->shear, data->f_length_kb,1098+ data->orthocorr, data->aspect,1099+ piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED);1100+1101+ // clipping offset1102+ const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl);1103+ const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct);1104+ const float cx = roi_out->scale * fullwidth * data->cl;1105+ const float cy = roi_out->scale * fullheight * data->ct;1106+1107+ DT_OMP_FOR(shared(ihomograph))1108+ // go over all pixels of output image1109+ for(int j = 0; j < roi_out->height; j++)1110+ {1111+ float *const restrict _out = out + (size_t)j * roi_out->width;1112+ for(int i = 0; i < roi_out->width; i++)1113+ {1114+ float DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL pout[3];1115+1116+ // convert output pixel coordinates to original image coordinates1117+ pout[0] = roi_out->x + i + cx;1118+ pout[1] = roi_out->y + j + cy;1119+ pout[0] /= roi_out->scale;1120+ pout[1] /= roi_out->scale;1121+ pout[2] = 1.0f;1122+1123+ // apply homograph1124+ mat3mulv(pin, (float *)ihomograph, pout);1125+1126+ // convert to input pixel coordinates1127+ pin[0] /= pin[2];1128+ pin[1] /= pin[2];1129+ pin[0] *= roi_in->scale;1130+ pin[1] *= roi_in->scale;1131+ pin[0] -= roi_in->x;1132+ pin[1] -= roi_in->y;1133+1134+ // get output values by interpolation from input image1135+ _out[i] = CLIP(dt_interpolation_compute_sample(interpolation, in,1136+ pin[0], pin[1],1137+ roi_in->width, roi_in->height, 1, roi_in->width));1138+ }1139+ }1140+}1141+1142+void modify_roi_out(dt_iop_module_t *self,1143+ dt_dev_pixelpipe_iop_t *piece,1144+ dt_iop_roi_t *roi_out,1145+ const dt_iop_roi_t *roi_in)1146+{1147+ const dt_iop_ashift_data_t *data = piece->data;1148+ *roi_out = *roi_in;1149+1150+ // nothing more to be done if parameters are set to neutral values1151+ if(_isneutral(data)) return;1152+1153+ float DT_ALIGNED_ARRAY homograph[3][3];1154+ _homography((float *)homograph, data->rotation, data->lensshift_v, data->lensshift_h,1155+ data->shear, data->f_length_kb,1156+ data->orthocorr, data->aspect,1157+ piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_FORWARD);1158+1159+ float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX;1160+1161+ // go through all four vertices of input roi and convert coordinates to output1162+ for(int y = 0; y < roi_in->height; y += roi_in->height - 1)1163+ {1164+ for(int x = 0; x < roi_in->width; x += roi_in->width - 1)1165+ {1166+ float DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL pout[3];1167+1168+ // convert from input coordinates to original image coordinates1169+ pin[0] = roi_in->x + x;1170+ pin[1] = roi_in->y + y;1171+ pin[0] /= roi_in->scale;1172+ pin[1] /= roi_in->scale;1173+ pin[2] = 1.0f;1174+1175+ // apply homograph1176+ mat3mulv(pout, (float *)homograph, pin);1177+1178+ // convert to output image coordinates1179+ pout[0] /= pout[2];1180+ pout[1] /= pout[2];1181+ pout[0] *= roi_out->scale;1182+ pout[1] *= roi_out->scale;1183+ xm = MIN(xm, pout[0]);1184+ xM = MAX(xM, pout[0]);1185+ ym = MIN(ym, pout[1]);1186+ yM = MAX(yM, pout[1]);1187+ }1188+ }1189+1190+ const float width = (xM - xm + 1.0f) * (data->cr - data->cl);1191+ const float height = (yM - ym + 1.0f) * (data->cb - data->ct);1192+1193+ roi_out->width = floorf(width);1194+ roi_out->height = floorf(height);1195+1196+ if(roi_out->width < 4 || roi_out->height < 4)1197+ {1198+ dt_print_pipe(DT_DEBUG_PIPE,1199+ "insane data", piece->pipe, self, DT_DEVICE_NONE, roi_in, roi_out);1200+ if(dt_pipe_is_full(piece->pipe))1201+ {1202+ dt_control_log1203+ (_("module '%s' has insane data so it is bypassed for now."1204+ " you should disable it or change parameters\n"),1205+ self->name());1206+ }1207+ roi_out->width = roi_in->width;1208+ roi_out->height = roi_in->height;1209+ piece->enabled = FALSE;1210+ }1211+}1212+1213+void modify_roi_in(dt_iop_module_t *self,1214+ dt_dev_pixelpipe_iop_t *piece,1215+ const dt_iop_roi_t *const roi_out,1216+ dt_iop_roi_t *roi_in)1217+{1218+ const dt_iop_ashift_data_t *data = piece->data;1219+ *roi_in = *roi_out;1220+1221+ // nothing more to be done if parameters are set to neutral values1222+ if(_isneutral(data)) return;1223+1224+ float DT_ALIGNED_ARRAY ihomograph[3][3];1225+ _homography((float *)ihomograph, data->rotation, data->lensshift_v, data->lensshift_h,1226+ data->shear, data->f_length_kb,1227+ data->orthocorr, data->aspect,1228+ piece->buf_in.width, piece->buf_in.height, ASHIFT_HOMOGRAPH_INVERTED);1229+1230+ const float orig_w = roi_in->scale * piece->buf_in.width;1231+ const float orig_h = roi_in->scale * piece->buf_in.height;1232+1233+ // clipping offset1234+ const float fullwidth = (float)piece->buf_out.width / (data->cr - data->cl);1235+ const float fullheight = (float)piece->buf_out.height / (data->cb - data->ct);1236+ const float cx = roi_out->scale * fullwidth * data->cl;1237+ const float cy = roi_out->scale * fullheight * data->ct;1238+1239+ float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX;1240+1241+ // go through all four vertices of output roi and convert coordinates to input1242+ for(int y = 0; y < roi_out->height; y += roi_out->height - 1)1243+ {1244+ for(int x = 0; x < roi_out->width; x += roi_out->width - 1)1245+ {1246+ float DT_ALIGNED_PIXEL pin[3], DT_ALIGNED_PIXEL pout[3];1247+1248+ // convert from output image coordinates to original image coordinates1249+ pout[0] = roi_out->x + x + cx;1250+ pout[1] = roi_out->y + y + cy;1251+ pout[0] /= roi_out->scale;1252+ pout[1] /= roi_out->scale;1253+ pout[2] = 1.0f;1254+1255+ // apply homograph1256+ mat3mulv(pin, (float *)ihomograph, pout);1257+1258+ // convert to input image coordinates1259+ pin[0] /= pin[2];1260+ pin[1] /= pin[2];1261+ pin[0] *= roi_in->scale;1262+ pin[1] *= roi_in->scale;1263+ xm = MIN(xm, pin[0]);1264+ xM = MAX(xM, pin[0]);1265+ ym = MIN(ym, pin[1]);1266+ yM = MAX(yM, pin[1]);1267+ }1268+ }1269+1270+ const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);1271+1272+ const float iw1 = interpolation->width;1273+ const float iw2 = 2.0f * iw1;1274+ roi_in->x = xm - iw1;1275+ roi_in->y = ym - iw1;1276+ roi_in->width = xM + iw2 - xm + 1.0f;1277+ roi_in->height = yM + iw2 - ym + 1.0f;1278+1279+1280+ // sanity check.1281+ roi_in->x = CLAMP(roi_in->x, 0, (int)floorf(orig_w));1282+ roi_in->y = CLAMP(roi_in->y, 0, (int)floorf(orig_h));1283+ roi_in->width = CLAMP(roi_in->width, 4, (int)floorf(orig_w) - roi_in->x);1284+ roi_in->height = CLAMP(roi_in->height, 4, (int)floorf(orig_h) - roi_in->y);1285+}1286+1287+// simple conversion of rgb image into greyscale variant suitable for1288+// line segment detection the lsd routines expect input as *double,1289+// roughly in the range [0.0; 256.0]1290+static void rgb2grey256(const float *const in,1291+ double *const out,1292+ const int width,1293+ const int height)1294+{1295+ const size_t npixels = (size_t)width * height;1296+1297+ DT_OMP_FOR()1298+ for(size_t index = 0; index < npixels; index++)1299+ {1300+ out[index] = (0.3f * in[4*index+0]1301+ + 0.59f * in[4*index+1]1302+ + 0.11f * in[4*index+2]) * 256.0;1303+ }1304+}1305+1306+// sobel edge enhancement in one direction1307+static void edge_enhance_1d(const double *in, double *out,1308+ const int width,1309+ const int height,1310+ const dt_iop_ashift_enhance_t dir)1311+{1312+ // Sobel kernels for both directions1313+ const double hkernel[3][3] = { { 1.0, 0.0, -1.0 },1314+ { 2.0, 0.0, -2.0 },1315+ { 1.0, 0.0, -1.0 } };1316+1317+ const double vkernel[3][3] = { { 1.0, 2.0, 1.0 },1318+ { 0.0, 0.0, 0.0 },1319+ { -1.0, -2.0, -1.0 } };1320+ const int kwidth = 3;1321+ const int khwidth = kwidth / 2;1322+1323+ // select kernel1324+ const double *kernel = (dir == ASHIFT_ENHANCE_HORIZONTAL)1325+ ? (const double *)hkernel1326+ : (const double *)vkernel;1327+1328+ DT_OMP_FOR()1329+ // loop over image pixels and perform sobel convolution1330+ for(int j = khwidth; j < height - khwidth; j++)1331+ {1332+ const double *inp = in + (size_t)j * width + khwidth;1333+ double *outp = out + (size_t)j * width + khwidth;1334+ for(int i = khwidth; i < width - khwidth; i++, inp++, outp++)1335+ {1336+ double sum = 0.0f;1337+ for(int jj = 0; jj < kwidth; jj++)1338+ {1339+ const int k = jj * kwidth;1340+ const int l = (jj - khwidth) * width;1341+ for(int ii = 0; ii < kwidth; ii++)1342+ {1343+ sum += inp[l + ii - khwidth] * kernel[k + ii];1344+ }1345+ }1346+ *outp = sum;1347+ }1348+ }1349+1350+ DT_OMP_FOR()1351+ // border fill in output buffer, so we don't get pseudo lines at image frame1352+ for(int j = 0; j < height; j++)1353+ for(int i = 0; i < width; i++)1354+ {1355+ double val = out[(size_t)j * width + i];1356+1357+ if(j < khwidth)1358+ val = out[(size_t)(khwidth - j) * width + i];1359+ else if(j >= height - khwidth)1360+ val = out[(size_t)(j - khwidth) * width + i];1361+ else if(i < khwidth)1362+ val = out[(size_t)j * width + (khwidth - i)];1363+ else if(i >= width - khwidth)1364+ val = out[(size_t)j * width + (i - khwidth)];1365+1366+ out[(size_t)j * width + i] = val;1367+1368+ // jump over center of image1369+ if(i == khwidth && j >= khwidth && j < height - khwidth) i = width - khwidth;1370+ }1371+}1372+1373+// edge enhancement in both directions1374+static gboolean edge_enhance(const double *in,1375+ double *out,1376+ const int width,1377+ const int height)1378+{1379+ double *Gx = NULL;1380+ double *Gy = NULL;1381+1382+ Gx = malloc(sizeof(double) * width * height);1383+ if(Gx == NULL) goto error;1384+1385+ Gy = malloc(sizeof(double) * width * height);1386+ if(Gy == NULL) goto error;1387+1388+ // perform edge enhancement in both directions1389+ edge_enhance_1d(in, Gx, width, height, ASHIFT_ENHANCE_HORIZONTAL);1390+ edge_enhance_1d(in, Gy, width, height, ASHIFT_ENHANCE_VERTICAL);1391+1392+// calculate absolute values1393+ DT_OMP_FOR()1394+ for(size_t k = 0; k < (size_t)width * height; k++)1395+ {1396+ out[k] = sqrt(Gx[k] * Gx[k] + Gy[k] * Gy[k]);1397+ }1398+1399+ free(Gx);1400+ free(Gy);1401+ return TRUE;1402+1403+error:1404+ if(Gx) free(Gx);1405+ if(Gy) free(Gy);1406+ return FALSE;1407+}1408+1409+// detail enhancement via bilateral grid (function arguments in and1410+// out may represent identical buffers)1411+static gboolean detail_enhance(const float *const in,1412+ float *const out,1413+ const int width,1414+ const int height)1415+{1416+ const float sigma_r = 5.0f;1417+ const float sigma_s = fminf(width, height) * 0.02f;1418+ const float detail = 10.0f;1419+ const size_t npixels = (size_t)width * height;1420+ gboolean success = TRUE;1421+1422+ // we need to convert from RGB to Lab first;1423+ // as colors don't matter we are safe to assume data to be sRGB1424+1425+ // convert RGB input to Lab, use output buffer for intermediate storage1426+ DT_OMP_FOR()1427+ for(size_t index = 0; index < 4*npixels; index += 4)1428+ {1429+ dt_aligned_pixel_t XYZ;1430+ dt_Rec709_to_XYZ_D50(in + index, XYZ); // convert linear sRBG to XYZ1431+ dt_XYZ_to_Lab(XYZ, out + index);1432+ }1433+1434+ // bilateral grid detail enhancement1435+ dt_bilateral_t *b = dt_bilateral_init(width, height, sigma_s, sigma_r);1436+1437+ if(b != NULL)1438+ {1439+ dt_bilateral_splat(b, out);1440+ dt_bilateral_blur(b);1441+ dt_bilateral_slice_to_output(b, out, out, detail);1442+ dt_bilateral_free(b);1443+ }1444+ else1445+ success = FALSE;1446+1447+ // convert resulting Lab to RGB output1448+ DT_OMP_FOR()1449+ for(size_t index = 0; index < 4*npixels; index += 4)1450+ {1451+ dt_aligned_pixel_t XYZ;1452+ dt_Lab_to_XYZ(out + index, XYZ);1453+ dt_XYZ_to_linearRGB(XYZ, out + index);1454+ }1455+1456+ return success;1457+}1458+1459+// apply gamma correction to RGB buffer (function arguments in and out1460+// may represent identical buffers)1461+static void gamma_correct(const float *const in,1462+ float *const out,1463+ const int width,1464+ const int height)1465+{1466+ const size_t npixels = (size_t)width * height;1467+ DT_OMP_FOR()1468+ for(size_t index = 0; index < 4 * npixels; index += 4)1469+ {1470+ for_three_channels(c)1471+ out[index+c] = powf(in[index+c], LSD_GAMMA);1472+ }1473+}1474+1475+// do actual line_detection based on LSD algorithm and return results according1476+// to this module's conventions1477+static gboolean line_detect(float *in,1478+ const int width,1479+ const int height,1480+ const int x_off,1481+ const int y_off,1482+ const float scale,1483+ dt_iop_ashift_line_t **alines,1484+ int *lcount,1485+ int *vcount,1486+ int *hcount,1487+ float *vweight,1488+ float *hweight,1489+ const dt_iop_ashift_enhance_t enhance,1490+ const gboolean is_raw)1491+{1492+ double *greyscale = NULL;1493+ double *lsd_lines = NULL;1494+ dt_iop_ashift_line_t *ashift_lines = NULL;1495+1496+ int vertical_count = 0;1497+ int horizontal_count = 0;1498+ float vertical_weight = 0.0f;1499+ float horizontal_weight = 0.0f;1500+1501+ // apply gamma correction if image is raw1502+ if(is_raw)1503+ {1504+ gamma_correct(in, in, width, height);1505+ }1506+1507+ // if requested perform an additional detail enhancement step1508+ if(enhance & ASHIFT_ENHANCE_DETAIL)1509+ {1510+ (void)detail_enhance(in, in, width, height);1511+ }1512+1513+ // allocate intermediate buffers1514+ greyscale = malloc(sizeof(double) * width * height);1515+ if(greyscale == NULL) goto error;1516+1517+ // convert to greyscale image1518+ rgb2grey256(in, greyscale, width, height);1519+1520+ // if requested perform an additional edge enhancement step1521+ if(enhance & ASHIFT_ENHANCE_EDGES)1522+ {1523+ (void)edge_enhance(greyscale, greyscale, width, height);1524+ }1525+1526+ // call the line segment detector LSD;1527+ // LSD stores the number of found lines in lines_count.1528+ // it returns structural details as vector 'double lines[7 * lines_count]'1529+ int lines_count;1530+1531+ lsd_lines = LineSegmentDetection(&lines_count, greyscale, width, height,1532+ LSD_SCALE, LSD_SIGMA_SCALE, LSD_QUANT,1533+ LSD_ANG_TH, LSD_LOG_EPS, LSD_DENSITY_TH,1534+ LSD_N_BINS, NULL, NULL, NULL);1535+1536+ // we count the lines that we really want to use1537+ int lct = 0;1538+ if(lines_count > 0)1539+ {1540+ // aggregate lines data into our own structures1541+ ashift_lines = malloc(sizeof(dt_iop_ashift_line_t) * lines_count);1542+1543+ if(ashift_lines == NULL) goto error;1544+1545+ for(int n = 0; n < lines_count; n++)1546+ {1547+ const float x1 = lsd_lines[n * 7 + 0];1548+ const float y1 = lsd_lines[n * 7 + 1];1549+ const float x2 = lsd_lines[n * 7 + 2];1550+ const float y2 = lsd_lines[n * 7 + 3];1551+1552+ // check for lines running along image borders and skip them.1553+ // these would likely be false-positives which could result1554+ // from any kind of processing artifacts1555+ if((fabsf(x1 - x2) < 1 && fmaxf(x1, x2) < 2)1556+ || (fabsf(x1 - x2) < 1 && fminf(x1, x2) > width - 3)1557+ || (fabsf(y1 - y2) < 1 && fmaxf(y1, y2) < 2)1558+ || (fabsf(y1 - y2) < 1 && fminf(y1, y2) > height - 3))1559+ continue;1560+1561+ // line position in absolute coordinates1562+ float px1 = x_off + x1;1563+ float py1 = y_off + y1;1564+ float px2 = x_off + x2;1565+ float py2 = y_off + y2;1566+1567+ // scale back to input buffer1568+ px1 /= scale;1569+ py1 /= scale;1570+ px2 /= scale;1571+ py2 /= scale;1572+1573+ // store as homogeneous coordinates1574+ ashift_lines[lct].p1[0] = px1;1575+ ashift_lines[lct].p1[1] = py1;1576+ ashift_lines[lct].p1[2] = 1.0f;1577+ ashift_lines[lct].p2[0] = px2;1578+ ashift_lines[lct].p2[1] = py2;1579+ ashift_lines[lct].p2[2] = 1.0f;1580+1581+ // calculate homogeneous coordinates of connecting line (defined by the two points)1582+ vec3prodn(ashift_lines[lct].L, ashift_lines[lct].p1, ashift_lines[lct].p2);1583+1584+ // normalaze line coordinates so that x^2 + y^2 = 11585+ // (this will always succeed as L is a real line connecting two real points)1586+ vec3lnorm(ashift_lines[lct].L, ashift_lines[lct].L);1587+1588+ // length and width of rectangle (see LSD)1589+ ashift_lines[lct].length = sqrt((px2 - px1) * (px2 - px1)1590+ + (py2 - py1) * (py2 - py1));1591+ ashift_lines[lct].width = lsd_lines[n * 7 + 4] / scale;1592+1593+ // ... and weight (= length * width * angle precision)1594+ const float weight = ashift_lines[lct].length1595+ * ashift_lines[lct].width1596+ * lsd_lines[n * 7 + 5];1597+1598+ ashift_lines[lct].weight = weight;1599+1600+1601+ const float angle = rad2degf(atan2f(py2 - py1, px2 - px1));1602+ const gboolean vertical = fabsf(fabsf(angle) - 90.0f) < MAX_TANGENTIAL_DEVIATION;1603+ const gboolean horizontal = fabsf(fabsf(fabsf(angle) - 90.0f) - 90.0f) < MAX_TANGENTIAL_DEVIATION;1604+ const gboolean relevant = ashift_lines[lct].length > MIN_LINE_LENGTH;1605+1606+ // register type of line1607+ dt_iop_ashift_linetype_t type = ASHIFT_LINE_IRRELEVANT;1608+1609+ if(vertical && relevant)1610+ {1611+ type = ASHIFT_LINE_VERTICAL_SELECTED;1612+ vertical_count++;1613+ vertical_weight += weight;1614+ }1615+ else if(horizontal && relevant)1616+ {1617+ type = ASHIFT_LINE_HORIZONTAL_SELECTED;1618+ horizontal_count++;1619+ horizontal_weight += weight;1620+ }1621+ ashift_lines[lct].type = type;1622+1623+ // the next valid line1624+ lct++;1625+ }1626+ }1627+#ifdef ASHIFT_DEBUG1628+ printf("%d lines (vertical %d, horizontal %d, not relevant %d)\n",1629+ lines_count, vertical_count,1630+ horizontal_count, lct - vertical_count - horizontal_count);1631+ float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN;1632+ for(int n = 0; n < lct; n++)1633+ {1634+ xmin = fmin(xmin, fmin(ashift_lines[n].p1[0], ashift_lines[n].p2[0]));1635+ xmax = fmax(xmax, fmax(ashift_lines[n].p1[0], ashift_lines[n].p2[0]));1636+ ymin = fmin(ymin, fmin(ashift_lines[n].p1[1], ashift_lines[n].p2[1]));1637+ ymax = fmax(ymax, fmax(ashift_lines[n].p1[1], ashift_lines[n].p2[1]));1638+ printf("x1 %.0f, y1 %.0f, x2 %.0f, y2 %.0f,"1639+ " length %.0f, width %f, X %f, Y %f, Z %f, type %d, scalars %f %f\n",1640+ ashift_lines[n].p1[0], ashift_lines[n].p1[1], ashift_lines[n].p2[0],1641+ ashift_lines[n].p2[1],1642+ ashift_lines[n].length, ashift_lines[n].width,1643+ ashift_lines[n].L[0], ashift_lines[n].L[1], ashift_lines[n].L[2],1644+ ashift_lines[n].type,1645+ vec3scalar(ashift_lines[n].p1, ashift_lines[n].L),1646+ vec3scalar(ashift_lines[n].p2, ashift_lines[n].L));1647+ }1648+ printf("xmin %.0f, xmax %.0f, ymin %.0f, ymax %.0f\n", xmin, xmax, ymin, ymax);1649+#endif1650+1651+ // store results in provided locations1652+ *lcount = lct;1653+ *vcount = vertical_count;1654+ *vweight = vertical_weight;1655+ *hcount = horizontal_count;1656+ *hweight = horizontal_weight;1657+ *alines = ashift_lines;1658+1659+ // free intermediate buffers1660+ free(lsd_lines);1661+ free(greyscale);1662+ return lct > 0;1663+1664+error:1665+ free(lsd_lines);1666+ free(greyscale);1667+ return FALSE;1668+}1669+1670+// get image from buffer, analyze for structure and save results1671+static int _get_structure(dt_iop_module_t *self,1672+ const dt_iop_ashift_enhance_t enhance)1673+{1674+ dt_iop_ashift_gui_data_t *g = self->gui_data;1675+1676+ float *buffer = NULL;1677+ int width = 0;1678+ int height = 0;1679+ int x_off = 0;1680+ int y_off = 0;1681+ float scale = 0.0f;1682+1683+ dt_iop_gui_enter_critical_section(self);1684+ // read buffer data if they are available1685+ if(g->buf != NULL)1686+ {1687+ width = g->buf_width;1688+ height = g->buf_height;1689+ x_off = g->buf_x_off;1690+ y_off = g->buf_y_off;1691+ scale = g->buf_scale;1692+1693+ // create a temporary buffer to hold image data1694+ buffer = dt_alloc_align_float((size_t)4 * width * height);1695+ if(buffer != NULL)1696+ dt_iop_image_copy_by_size(buffer, g->buf, width, height, 4);1697+ }1698+ dt_iop_gui_leave_critical_section(self);1699+1700+ if(buffer == NULL) goto error;1701+1702+ // get rid of old structural data1703+ g->lines_count = 0;1704+ g->vertical_count = 0;1705+ g->horizontal_count = 0;1706+ free(g->lines);1707+ g->lines = NULL;1708+1709+ dt_iop_ashift_line_t *lines;1710+ int lines_count;1711+ int vertical_count;1712+ int horizontal_count;1713+ float vertical_weight;1714+ float horizontal_weight;1715+1716+ // get new structural data1717+ if(!line_detect(buffer, width, height, x_off, y_off, scale, &lines, &lines_count,1718+ &vertical_count, &horizontal_count, &vertical_weight, &horizontal_weight,1719+ enhance, dt_image_is_raw(&self->dev->image_storage)))1720+ goto error;1721+1722+ // save new structural data1723+ g->lines_in_width = width;1724+ g->lines_in_height = height;1725+ g->lines_x_off = x_off;1726+ g->lines_y_off = y_off;1727+ g->lines_count = lines_count;1728+ g->vertical_count = vertical_count;1729+ g->horizontal_count = horizontal_count;1730+ g->vertical_weight = vertical_weight;1731+ g->horizontal_weight = horizontal_weight;1732+ g->lines_version++;1733+ g->lines = lines;1734+1735+ dt_free_align(buffer);1736+ return TRUE;1737+1738+error:1739+ dt_free_align(buffer);1740+ return FALSE;1741+}1742+1743+1744+// swap two integer values1745+static inline void swap(int *a, int *b)1746+{1747+ const int tmp = *a;1748+ *a = *b;1749+ *b = tmp;1750+}1751+1752+// do complete permutations1753+static gboolean quickperm(int *a, int *p, const int N, int *i)1754+{1755+ if(*i >= N) return FALSE;1756+1757+ p[*i]--;1758+ const int j = (*i % 2 == 1) ? p[*i] : 0;1759+ swap(&a[j], &a[*i]);1760+ *i = 1;1761+ while(p[*i] == 0)1762+ {1763+ p[*i] = *i;1764+ (*i)++;1765+ }1766+ return TRUE;1767+}1768+1769+// Fisher-Yates shuffle1770+static void shuffle(int *a, const int N)1771+{1772+ for(int i = 0; i < N; i++)1773+ {1774+ const int j = i + rand() % (N - i);1775+ swap(&a[j], &a[i]);1776+ }1777+}1778+1779+// factorial function1780+static int fact(const int n)1781+{1782+ return (n == 1 ? 1 : n * fact(n - 1));1783+}1784+1785+// We use a pseudo-RANSAC algorithm to elminiate ouliers from our set of lines. The1786+// original RANSAC works on linear optimization problems. Our model is nonlinear. We1787+// take advantage of the fact that lines interesting for our model are vantage lines1788+// that meet in one vantage point for each subset of lines (vertical/horizontal).1789+// Strategy: we construct a model by (random) sampling within the subset of lines and1790+// calculate the vantage point. Then we check the "distance" of all other lines to the1791+// vantage point. The model that gives highest number of lines combined with the highest1792+// total weight and lowest overall "distance" wins.1793+// Disadvantage: compared to the original RANSAC we don't get any model parameters that1794+// we could use for the following NMS fit.1795+//1796+// Self-tuning: we optimize "epsilon", the hurdle rate to reject a1797+// line as an outlier, by a number of dry runs first. The target1798+// average percentage value of lines to eliminate as outliers (without1799+// judging on the quality of the model) is given by1800+// RANSAC_ELIMINATION_RATIO,1801+//1802+// note: the actual percentage of outliers removed in the final run1803+// will be lower because we will finally look for the best quality1804+// model with the optimized epsilon and that quality value also1805+// encloses the number of good lines.1806+1807+static void ransac(const dt_iop_ashift_line_t *lines,1808+ int *index_set,1809+ int *inout_set,1810+ const int set_count,1811+ const float total_weight,1812+ const int xmin,1813+ const int xmax,1814+ const int ymin,1815+ const int ymax)1816+{1817+ if(set_count < 3) return;1818+1819+ const size_t set_size = set_count * sizeof(int);1820+ int *best_set = malloc(set_size);1821+ memcpy(best_set, index_set, set_size);1822+ int *best_inout = calloc(1, set_size);1823+1824+ float best_quality = 0.0f;1825+1826+ // hurdle value epsilon for rejecting a line as an outlier will be self-tuning1827+ // in a number of dry runs1828+ float epsilon = powf(10.0f, -RANSAC_EPSILON);1829+ float epsilon_step = RANSAC_EPSILON_STEP;1830+ // some accounting variables for self-tuning1831+ int lines_eliminated = 0;1832+ int valid_runs = 0;1833+1834+ // number of runs to optimize epsilon1835+ const int optiruns = RANSAC_OPTIMIZATION_STEPS * RANSAC_OPTIMIZATION_DRY_RUNS;1836+ // go for complete permutations on small set sizes, else for random sample consensus1837+ const int riter = (set_count > RANSAC_HURDLE) ? RANSAC_RUNS : fact(set_count);1838+1839+ // some data needed for quickperm1840+ int *perm = malloc(sizeof(int) * (set_count + 1));1841+ for(int n = 0; n < set_count + 1; n++) perm[n] = n;1842+ int piter = 1;1843+1844+ // inout holds good/bad qualification for each line1845+ int *inout = malloc(set_size);1846+1847+ for(int r = 0; r < optiruns + riter; r++)1848+ {1849+ // get random or systematic variation of index set1850+ if(set_count > RANSAC_HURDLE || r < optiruns)1851+ shuffle(index_set, set_count);1852+ else1853+ (void)quickperm(index_set, perm, set_count, &piter);1854+1855+ // summed quality evaluation of this run1856+ float quality = 0.0f;1857+1858+ // we build a model ouf of the first two lines1859+ const float *L1 = lines[index_set[0]].L;1860+ const float *L2 = lines[index_set[1]].L;1861+1862+ // get intersection point (ideally a vantage point)1863+ float DT_ALIGNED_PIXEL V[3];1864+ vec3prodn(V, L1, L2);1865+1866+ // catch special cases:1867+ // a) L1 and L2 are identical -> V is NULL -> no valid vantage point1868+ // b) vantage point lies inside image frame (no chance to correct for this case)1869+ if(vec3isnull(V) ||1870+ (fabsf(V[2]) > 0.0f &&1871+ V[0]/V[2] >= xmin &&1872+ V[1]/V[2] >= ymin &&1873+ V[0]/V[2] <= xmax &&1874+ V[1]/V[2] <= ymax))1875+ {1876+ // no valid model1877+ quality = 0.0f;1878+ }1879+ else1880+ {1881+ // valid model1882+1883+ // normalize V so that x^2 + y^2 + z^2 = 11884+ vec3norm(V, V);1885+1886+ // the two lines constituting the model are part of the set1887+ inout[0] = 1;1888+ inout[1] = 1;1889+1890+ // go through all remaining lines, check if they are within the model, and1891+ // mark that fact in inout[].1892+ // summarize a quality parameter for all lines within the model1893+ for(int n = 2; n < set_count; n++)1894+ {1895+ // L is normalized so that x^2 + y^2 = 11896+ const float *L3 = lines[index_set[n]].L;1897+1898+ // we take the absolute value of the dot product of V and L as1899+ // a measure of the "distance" between point and line. Note1900+ // that this is not the real euclidean distance but - with the1901+ // given normalization - just a pragmatically selected number1902+ // that goes to zero if V lies on L and increases the more V1903+ // and L are apart1904+ const float d = fabsf(vec3scalar(V, L3));1905+1906+ // depending on d we either include or exclude the point from the set1907+ inout[n] = (d < epsilon) ? 1 : 0;1908+1909+ float q;1910+1911+ if(inout[n] == 1)1912+ {1913+ // a quality parameter that depends 1/3 on the number of1914+ // lines within the model, 1/3 on their weight, and 1/3 on1915+ // their weighted distance d to the vantage point1916+ q = 0.33f / (float)set_count1917+ + 0.33f * lines[index_set[n]].weight / total_weight1918+ + 0.33f * (1.0f - d / epsilon)1919+ * (float)set_count * lines[index_set[n]].weight / total_weight;1920+ }1921+ else1922+ {1923+ q = 0.0f;1924+ lines_eliminated++;1925+ }1926+1927+ quality += q;1928+ }1929+ valid_runs++;1930+ }1931+1932+ if(r < optiruns)1933+ {1934+ // on last run of each self-tuning step1935+ if((r % RANSAC_OPTIMIZATION_DRY_RUNS) == (RANSAC_OPTIMIZATION_DRY_RUNS - 1)1936+ && (valid_runs > 0))1937+ {1938+#ifdef ASHIFT_DEBUG1939+ printf("ransac self-tuning (run %d): epsilon %f", r, epsilon);1940+#endif1941+ // average ratio of lines that we eliminated with the given epsilon1942+ float ratio = 100.0f * (float)lines_eliminated / ((float)set_count * valid_runs);1943+ // adjust epsilon accordingly1944+ if(ratio < RANSAC_ELIMINATION_RATIO)1945+ epsilon = powf(10.0f, log10(epsilon) - epsilon_step);1946+ else if(ratio > RANSAC_ELIMINATION_RATIO)1947+ epsilon = powf(10.0f, log10(epsilon) + epsilon_step);1948+#ifdef ASHIFT_DEBUG1949+ printf(" (elimination ratio %f) -> %f\n", ratio, epsilon);1950+#endif1951+ // reduce step-size for next optimization round1952+ epsilon_step /= 2.0f;1953+ lines_eliminated = 0;1954+ valid_runs = 0;1955+ }1956+ }1957+ else1958+ {1959+ // in the "real" runs check against the best model found so far1960+ if(quality > best_quality)1961+ {1962+ memcpy(best_set, index_set, set_size);1963+ memcpy(best_inout, inout, set_size);1964+ best_quality = quality;1965+ }1966+ }1967+1968+#ifdef ASHIFT_DEBUG1969+ // report some statistics1970+ int count = 0, lastcount = 0;1971+ for(int n = 0; n < set_count; n++) count += best_inout[n];1972+ for(int n = 0; n < set_count; n++) lastcount += inout[n];1973+ printf("ransac run %d: best qual %.6f, eps %.6f,"1974+ " line count %d of %d (this run: qual %.5f, count %d (%2f%%))\n", r,1975+ best_quality, epsilon, count, set_count, quality, lastcount,1976+ 100.0f * lastcount / (float)set_count);1977+#endif1978+ }1979+1980+ // store back best set1981+ memcpy(index_set, best_set, set_size);1982+ memcpy(inout_set, best_inout, set_size);1983+1984+ free(inout);1985+ free(perm);1986+ free(best_inout);1987+ free(best_set);1988+}1989+1990+1991+// try to clean up structural data by eliminating outliers and thereby increasing1992+// the chance of a convergent fitting1993+static gboolean _remove_outliers(const dt_iop_module_t *self)1994+{1995+ dt_iop_ashift_gui_data_t *g = self->gui_data;1996+1997+ const int width = g->lines_in_width;1998+ const int height = g->lines_in_height;1999+ const int xmin = g->lines_x_off;2000+ const int ymin = g->lines_y_off;2001+ const int xmax = xmin + width;2002+ const int ymax = ymin + height;2003+2004+ if(g->lines_count < 1)2005+ return TRUE;2006+ // holds the index set of lines we want to work on2007+ int *lines_set = malloc(sizeof(int) * g->lines_count);2008+ // holds the result of ransac2009+ int *inout_set = malloc(sizeof(int) * g->lines_count);2010+2011+ if(!lines_set || !inout_set)2012+ goto error;2013+ // some accounting variables2014+ int vnb = 0, vcount = 0;2015+ int hnb = 0, hcount = 0;2016+2017+ // just to be on the safe side2018+ if(g->lines == NULL) goto error;2019+2020+ // generate index list for the vertical lines2021+ for(int n = 0; n < g->lines_count; n++)2022+ {2023+ // is this a selected vertical line?2024+ if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_VERTICAL_SELECTED)2025+ continue;2026+2027+ lines_set[vnb] = n;2028+ inout_set[vnb] = 0;2029+ vnb++;2030+ }2031+2032+ // it only makes sense to call ransac if we have more than two lines2033+ if(vnb > 2)2034+ ransac(g->lines, lines_set, inout_set, vnb, g->vertical_weight,2035+ xmin, xmax, ymin, ymax);2036+2037+ // adjust line selected flag according to the ransac results2038+ for(int n = 0; n < vnb; n++)2039+ {2040+ const int m = lines_set[n];2041+ if(inout_set[n] == 1)2042+ {2043+ g->lines[m].type |= ASHIFT_LINE_SELECTED;2044+ vcount++;2045+ }2046+ else2047+ g->lines[m].type &= ~ASHIFT_LINE_SELECTED;2048+ }2049+ // update number of vertical lines2050+ g->vertical_count = vcount;2051+ g->lines_version++;2052+2053+ // now generate index list for the horizontal lines2054+ for(int n = 0; n < g->lines_count; n++)2055+ {2056+ // is this a selected horizontal line?2057+ if((g->lines[n].type & ASHIFT_LINE_MASK) != ASHIFT_LINE_HORIZONTAL_SELECTED)2058+ continue;2059+2060+ lines_set[hnb] = n;2061+ inout_set[hnb] = 0;2062+ hnb++;2063+ }2064+2065+ // it only makes sense to call ransac if we have more than two lines2066+ if(hnb > 2)2067+ ransac(g->lines, lines_set, inout_set, hnb, g->horizontal_weight,2068+ xmin, xmax, ymin, ymax);2069+2070+ // adjust line selected flag according to the ransac results2071+ for(int n = 0; n < hnb; n++)2072+ {2073+ const int m = lines_set[n];2074+ if(inout_set[n] == 1)2075+ {2076+ g->lines[m].type |= ASHIFT_LINE_SELECTED;2077+ hcount++;2078+ }2079+ else2080+ g->lines[m].type &= ~ASHIFT_LINE_SELECTED;2081+ }2082+ // update number of horizontal lines2083+ g->horizontal_count = hcount;2084+ g->lines_version++;2085+2086+ free(inout_set);2087+ free(lines_set);2088+2089+ return TRUE;2090+2091+error:2092+ free(inout_set);2093+ free(lines_set);2094+ return FALSE;2095+}2096+2097+// utility function to map a variable in [min; max] to [-INF; + INF]2098+static inline double logit(const double x, const double min, const double max)2099+{2100+ const double eps = 1.0e-6;2101+ // make sure p does not touch the borders of its definition area,2102+ // not critical for data accuracy as logit() is only used on initial fit parameters2103+ const double p = CLAMP((x - min) / (max - min), eps, 1.0 - eps);2104+2105+ return (2.0 * atanh(2.0 * p - 1.0));2106+}2107+2108+// inverted function to logit()2109+static inline double ilogit(const double L, const double min, const double max)2110+{2111+ const double p = 0.5 * (1.0 + tanh(0.5 * L));2112+2113+ return (p * (max - min) + min);2114+}2115+2116+// helper function for simplex() return quality parameter for the given model2117+// strategy:2118+// * generate homography matrix out of fixed parameters and fitting parameters2119+// * apply homography to all end points of affected lines2120+// * generate new line out of transformed end points2121+// * calculate scalar product s of line with perpendicular axis2122+// * sum over weighted s^2 values2123+2124+static double model_fitness(double *params, void *data)2125+{2126+ const dt_iop_ashift_fit_params_t *fit = (dt_iop_ashift_fit_params_t *)data;2127+2128+ // just for convenience: get shorter names2129+ const dt_iop_ashift_line_t *lines = fit->lines;2130+ const int lines_count = fit->lines_count;2131+ const int width = fit->width;2132+ const int height = fit->height;2133+ const float f_length_kb = fit->f_length_kb;2134+ const float orthocorr = fit->orthocorr;2135+ const float aspect = fit->aspect;2136+2137+ float rotation = fit->rotation;2138+ float lensshift_v = fit->lensshift_v;2139+ float lensshift_h = fit->lensshift_h;2140+ float shear = fit->shear;2141+ const float rotation_range = fit->rotation_range;2142+ const float lensshift_v_range = fit->lensshift_v_range;2143+ const float lensshift_h_range = fit->lensshift_h_range;2144+ const float shear_range = fit->shear_range;2145+2146+ int pcount = 0;2147+2148+ // fill in fit parameters from params[]. Attention: order matters!!!2149+ if(dt_isnan(rotation))2150+ {2151+ rotation = ilogit(params[pcount], -rotation_range, rotation_range);2152+ pcount++;2153+ }2154+2155+ if(dt_isnan(lensshift_v))2156+ {2157+ lensshift_v = ilogit(params[pcount], -lensshift_v_range, lensshift_v_range);2158+ pcount++;2159+ }2160+2161+ if(dt_isnan(lensshift_h))2162+ {2163+ lensshift_h = ilogit(params[pcount], -lensshift_h_range, lensshift_h_range);2164+ pcount++;2165+ }2166+2167+ if(dt_isnan(shear))2168+ {2169+ shear = ilogit(params[pcount], -shear_range, shear_range);2170+ pcount++;2171+ }2172+2173+ assert(pcount == fit->params_count);2174+2175+ // the possible reference axes2176+ const float DT_ALIGNED_PIXEL Av[3] = { 1.0f, 0.0f, 0.0f };2177+ const float DT_ALIGNED_PIXEL Ah[3] = { 0.0f, 1.0f, 0.0f };2178+2179+ // generate homograph out of the parameters2180+ float DT_ALIGNED_ARRAY homograph[3][3];2181+ _homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, f_length_kb,2182+ orthocorr, aspect, width, height, ASHIFT_HOMOGRAPH_FORWARD);2183+2184+ // accounting variables2185+ double sumsq_v = 0.0;2186+ double sumsq_h = 0.0;2187+ double weight_v = 0.0;2188+ double weight_h = 0.0;2189+ int count_v = 0;2190+ int count_h = 0;2191+ int count = 0;2192+2193+ // iterate over all lines2194+ for(int n = 0; n < lines_count; n++)2195+ {2196+ // check if this is a line which we must skip2197+ if((lines[n].type & fit->linemask) != fit->linetype)2198+ continue;2199+2200+ // the direction of this line (vertical?)2201+ const int isvertical = lines[n].type & ASHIFT_LINE_DIRVERT;2202+2203+ // select the perpendicular reference axis2204+ const float *A = isvertical ? Ah : Av;2205+2206+ // apply homographic transformation to the end points2207+ float DT_ALIGNED_PIXEL P1[3];2208+ float DT_ALIGNED_PIXEL P2[3];2209+ mat3mulv(P1, (float *)homograph, lines[n].p1);2210+ mat3mulv(P2, (float *)homograph, lines[n].p2);2211+2212+ // get line connecting the two points2213+ float DT_ALIGNED_PIXEL L[3];2214+ vec3prodn(L, P1, P2);2215+2216+ // normalize L so that x^2 + y^2 = 1; makes sure that2217+ // y^2 = 1 / (1 + m^2) and x^2 = m^2 / (1 + m^2) with m defining the slope of the line2218+ vec3lnorm(L, L);2219+2220+ // get scalar product of line L with orthogonal axis A -> gives 02221+ // if line is perpendicular2222+ const float s = vec3scalar(L, A);2223+2224+ // sum up weighted s^2 for both directions individually2225+ sumsq_v += isvertical ? s * s * lines[n].weight : 0.0;2226+ weight_v += isvertical ? lines[n].weight : 0.0;2227+ count_v += isvertical ? 1 : 0;2228+ sumsq_h += !isvertical ? s * s * lines[n].weight : 0.0;2229+ weight_h += !isvertical ? lines[n].weight : 0.0;2230+ count_h += !isvertical ? 1 : 0;2231+ count++;2232+ }2233+2234+ const double v = weight_v > 0.0f && count > 02235+ ? sumsq_v / weight_v * (float)count_v / count2236+ : 0.0;2237+2238+ const double h = weight_h > 0.0f && count > 02239+ ? sumsq_h / weight_h * (float)count_h / count2240+ : 0.0;2241+2242+ double sum = sqrt(1.0 - (1.0 - v) * (1.0 - h)) * 1.0e6;2243+ //double sum = sqrt(v + h) * 1.0e6;2244+2245+#ifdef ASHIFT_DEBUG2246+ printf("fitness with rotation %f, lensshift_v %f,"2247+ " lensshift_h %f, shear %f -> lines %d, quality %10f\n",2248+ rotation, lensshift_v, lensshift_h, shear, count, sum);2249+#endif2250+2251+ return sum;2252+}2253+2254+// setup all data structures for fitting and call NM simplex2255+static dt_iop_ashift_nmsresult_t nmsfit(dt_iop_module_t *self,2256+ dt_iop_ashift_params_t *p,2257+ const dt_iop_ashift_fitaxis_t dir)2258+{2259+ const dt_iop_ashift_gui_data_t *g = self->gui_data;2260+2261+ if(!g->lines) return NMS_NOT_ENOUGH_LINES;2262+ if(dir == ASHIFT_FIT_NONE) return NMS_SUCCESS;2263+2264+ double params[4];2265+ int pcount = 0;2266+ int enough_lines = TRUE;2267+2268+ // initialize fit parameters2269+ dt_iop_ashift_fit_params_t fit;2270+ fit.lines = g->lines;2271+ fit.lines_count = g->lines_count;2272+ fit.width = g->lines_in_width;2273+ fit.height = g->lines_in_height;2274+ fit.f_length_kb = (p->mode == ASHIFT_MODE_GENERIC)2275+ ? DEFAULT_F_LENGTH2276+ : p->f_length * p->crop_factor;2277+ fit.orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr;2278+ fit.aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect;2279+ fit.rotation = p->rotation;2280+ fit.lensshift_v = p->lensshift_v;2281+ fit.lensshift_h = p->lensshift_h;2282+ fit.shear = p->shear;2283+ fit.rotation_range = g->rotation_range;2284+ fit.lensshift_v_range = g->lensshift_v_range;2285+ fit.lensshift_h_range = g->lensshift_h_range;2286+ fit.shear_range = g->shear_range;2287+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2288+ fit.linemask = ASHIFT_LINE_MASK;2289+ fit.params_count = 0;2290+ fit.weight = 0.0f;2291+2292+ // if the image is flipped and if we do not want to fit both lens shift2293+ // directions or none at all, then we need to change direction2294+ dt_iop_ashift_fitaxis_t mdir = dir;2295+ if((mdir & ASHIFT_FIT_LENS_BOTH) != ASHIFT_FIT_LENS_BOTH &&2296+ (mdir & ASHIFT_FIT_LENS_BOTH) != 0)2297+ {2298+ // flip all directions2299+ mdir ^= g->isflipped ? ASHIFT_FIT_FLIP : 0;2300+ // special case that needs to be corrected2301+ mdir |= (mdir & ASHIFT_FIT_LINES_BOTH) == 0 ? ASHIFT_FIT_LINES_BOTH : 0;2302+ }2303+2304+2305+ // prepare fit structure and starting parameters for simplex fit.2306+ // note: the sequence of parameters in params[] needs to match the2307+ // respective order in dt_iop_ashift_fit_params_t. Parameters which are2308+ // to be fittet are marked with NAN in the fit structure. Non-NAN2309+ // parameters are assumed to be constant.2310+ if(mdir & ASHIFT_FIT_ROTATION)2311+ {2312+ // we fit rotation2313+ fit.params_count++;2314+ params[pcount] = logit(fit.rotation, -fit.rotation_range, fit.rotation_range);2315+ pcount++;2316+ fit.rotation = NAN;2317+ }2318+2319+ if(mdir & ASHIFT_FIT_LENS_VERT)2320+ {2321+ // we fit vertical lens shift2322+ fit.params_count++;2323+ params[pcount] = logit(fit.lensshift_v, -fit.lensshift_v_range, fit.lensshift_v_range);2324+ pcount++;2325+ fit.lensshift_v = NAN;2326+ }2327+2328+ if(mdir & ASHIFT_FIT_LENS_HOR)2329+ {2330+ // we fit horizontal lens shift2331+ fit.params_count++;2332+ params[pcount] = logit(fit.lensshift_h, -fit.lensshift_h_range, fit.lensshift_h_range);2333+ pcount++;2334+ fit.lensshift_h = NAN;2335+ }2336+2337+ if(mdir & ASHIFT_FIT_SHEAR)2338+ {2339+ // we fit the shear parameter2340+ fit.params_count++;2341+ params[pcount] = logit(fit.shear, -fit.shear_range, fit.shear_range);2342+ pcount++;2343+ fit.shear = NAN;2344+ }2345+2346+ if(mdir & ASHIFT_FIT_LINES_VERT)2347+ {2348+ // we use vertical lines for fitting2349+ fit.linetype |= ASHIFT_LINE_DIRVERT;2350+ fit.weight += g->vertical_weight;2351+ enough_lines = enough_lines && (g->vertical_count >= MINIMUM_FITLINES);2352+ }2353+2354+ if(mdir & ASHIFT_FIT_LINES_HOR)2355+ {2356+ // we use horizontal lines for fitting2357+ fit.linetype |= 0;2358+ fit.weight += g->horizontal_weight;2359+ enough_lines = enough_lines && (g->horizontal_count >= MINIMUM_FITLINES);2360+ }2361+2362+ // this needs to come after ASHIFT_FIT_LINES_VERT and ASHIFT_FIT_LINES_HOR2363+ if((mdir & ASHIFT_FIT_LINES_BOTH) == ASHIFT_FIT_LINES_BOTH)2364+ {2365+ // if we use fitting in both directions we need to2366+ // adjust fit.linetype and fit.linemask to match all selected lines2367+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2368+ fit.linemask = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2369+ }2370+2371+ // error case: we do not run simplex if there are not enough lines2372+ if(!enough_lines)2373+ {2374+#ifdef ASHIFT_DEBUG2375+ printf("optimization not possible: insufficient number of lines\n");2376+#endif2377+ return NMS_NOT_ENOUGH_LINES;2378+ }2379+2380+ // start the simplex fit2381+ const int iter = simplex(model_fitness, params, fit.params_count,2382+ NMS_EPSILON, NMS_SCALE, NMS_ITERATIONS, NULL, (void*)&fit);2383+2384+ // error case: the fit did not converge2385+ if(iter >= NMS_ITERATIONS)2386+ {2387+#ifdef ASHIFT_DEBUG2388+ printf("optimization not successful: maximum number of iterations reached (%d)\n", iter);2389+#endif2390+ return NMS_DID_NOT_CONVERGE;2391+ }2392+2393+ // fit was successful: now consolidate the results (order matters!!!)2394+ pcount = 0;2395+ fit.rotation = dt_isnan(fit.rotation)2396+ ? ilogit(params[pcount++], -fit.rotation_range, fit.rotation_range)2397+ : fit.rotation;2398+2399+ fit.lensshift_v = dt_isnan(fit.lensshift_v)2400+ ? ilogit(params[pcount++], -fit.lensshift_v_range, fit.lensshift_v_range)2401+ : fit.lensshift_v;2402+2403+ fit.lensshift_h = dt_isnan(fit.lensshift_h)2404+ ? ilogit(params[pcount++], -fit.lensshift_h_range, fit.lensshift_h_range)2405+ : fit.lensshift_h;2406+2407+ fit.shear = dt_isnan(fit.shear)2408+ ? ilogit(params[pcount++], -fit.shear_range, fit.shear_range)2409+ : fit.shear;2410+2411+#ifdef ASHIFT_DEBUG2412+ printf("params after optimization (%d iterations): rotation %f,"2413+ " lensshift_v %f, lensshift_h %f, shear %f\n",2414+ iter, fit.rotation, fit.lensshift_v, fit.lensshift_h, fit.shear);2415+#endif2416+2417+ // sanity check: in case of extreme values the image gets distorted2418+ // so strongly that it spans an insanely huge area. we check that2419+ // case and assume values that increase the image area by more than2420+ // a factor of 4 as being insane.2421+ float DT_ALIGNED_ARRAY homograph[3][3];2422+ _homography((float *)homograph, fit.rotation, fit.lensshift_v, fit.lensshift_h,2423+ fit.shear, fit.f_length_kb,2424+ fit.orthocorr, fit.aspect, fit.width, fit.height, ASHIFT_HOMOGRAPH_FORWARD);2425+2426+ // visit all four corners and find maximum span2427+ float xm = FLT_MAX, xM = -FLT_MAX, ym = FLT_MAX, yM = -FLT_MAX;2428+ for(int y = 0; y < fit.height; y += fit.height - 1)2429+ for(int x = 0; x < fit.width; x += fit.width - 1)2430+ {2431+ float DT_ALIGNED_PIXEL pi[3], DT_ALIGNED_PIXEL po[3];2432+ pi[0] = x;2433+ pi[1] = y;2434+ pi[2] = 1.0f;2435+ mat3mulv(po, (float *)homograph, pi);2436+ po[0] /= po[2];2437+ po[1] /= po[2];2438+ xm = MIN(xm, po[0]);2439+ ym = MIN(ym, po[1]);2440+ xM = MAX(xM, po[0]);2441+ yM = MAX(yM, po[1]);2442+ }2443+2444+ if((xM - xm) * (yM - ym) > 4.0f * fit.width * fit.height)2445+ {2446+#ifdef ASHIFT_DEBUG2447+ printf("optimization not successful: degenerate case with"2448+ " area growth factor (%f) exceeding limits\n",2449+ (xM - xm) * (yM - ym) / (fit.width * fit.height));2450+#endif2451+ return NMS_INSANE;2452+ }2453+2454+ // now write the results into structure p2455+ p->rotation = fit.rotation;2456+ p->lensshift_v = fit.lensshift_v;2457+ p->lensshift_h = fit.lensshift_h;2458+ p->shear = fit.shear;2459+ return NMS_SUCCESS;2460+}2461+2462+#ifdef ASHIFT_DEBUG2463+// only used in development phase. call model_fitness() with current parameters and2464+// print some useful information2465+static void model_probe(dt_iop_module_t *self,2466+ dt_iop_ashift_params_t *p,2467+ const dt_iop_ashift_fitaxis_t dir)2468+{2469+ dt_iop_ashift_gui_data_t *g = self->gui_data;2470+2471+ if(!g->lines) return;2472+ if(dir == ASHIFT_FIT_NONE) return;2473+2474+ double params[4];2475+ int enough_lines = TRUE;2476+2477+ // initialize fit parameters2478+ dt_iop_ashift_fit_params_t fit;2479+ fit.lines = g->lines;2480+ fit.lines_count = g->lines_count;2481+ fit.width = g->lines_in_width;2482+ fit.height = g->lines_in_height;2483+ fit.f_length_kb = (p->mode == ASHIFT_MODE_GENERIC)2484+ ? DEFAULT_F_LENGTH2485+ : p->f_length * p->crop_factor;2486+ fit.orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr;2487+ fit.aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect;2488+ fit.rotation = p->rotation;2489+ fit.lensshift_v = p->lensshift_v;2490+ fit.lensshift_h = p->lensshift_h;2491+ fit.shear = p->shear;2492+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2493+ fit.linemask = ASHIFT_LINE_MASK;2494+ fit.params_count = 0;2495+ fit.weight = 0.0f;2496+2497+ // if the image is flipped and if we do not want to fit both lens shift2498+ // directions or none at all, then we need to change direction2499+ dt_iop_ashift_fitaxis_t mdir = dir;2500+ if((mdir & ASHIFT_FIT_LENS_BOTH) != ASHIFT_FIT_LENS_BOTH &&2501+ (mdir & ASHIFT_FIT_LENS_BOTH) != 0)2502+ {2503+ // flip all directions2504+ mdir ^= g->isflipped ? ASHIFT_FIT_FLIP : 0;2505+ // special case that needs to be corrected2506+ mdir |= (mdir & ASHIFT_FIT_LINES_BOTH) == 0 ? ASHIFT_FIT_LINES_BOTH : 0;2507+ }2508+2509+ if(mdir & ASHIFT_FIT_LINES_VERT)2510+ {2511+ // we use vertical lines for fitting2512+ fit.linetype |= ASHIFT_LINE_DIRVERT;2513+ fit.weight += g->vertical_weight;2514+ enough_lines = enough_lines && (g->vertical_count >= MINIMUM_FITLINES);2515+ }2516+2517+ if(mdir & ASHIFT_FIT_LINES_HOR)2518+ {2519+ // we use horizontal lines for fitting2520+ fit.linetype |= 0;2521+ fit.weight += g->horizontal_weight;2522+ enough_lines = enough_lines && (g->horizontal_count >= MINIMUM_FITLINES);2523+ }2524+2525+ // this needs to come after ASHIFT_FIT_LINES_VERT and ASHIFT_FIT_LINES_HOR2526+ if((mdir & ASHIFT_FIT_LINES_BOTH) == ASHIFT_FIT_LINES_BOTH)2527+ {2528+ // if we use fitting in both directions we need to2529+ // adjust fit.linetype and fit.linemask to match all selected lines2530+ fit.linetype = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2531+ fit.linemask = ASHIFT_LINE_RELEVANT | ASHIFT_LINE_SELECTED;2532+ }2533+2534+ double quality = model_fitness(params, (void *)&fit);2535+2536+ printf("model fitness: %.8f (rotation %f, lensshift_v %f, lensshift_h %f, shear %f)\n",2537+ quality, p->rotation, p->lensshift_v, p->lensshift_h, p->shear);2538+}2539+#endif2540+2541+// function to keep crop fitting parameters within constraints2542+static void crop_constraint(double *params, const int pcount)2543+{2544+ if(pcount > 0) params[0] = fabs(params[0]);2545+ if(pcount > 1) params[1] = fabs(params[1]);2546+ if(pcount > 2) params[2] = fabs(params[2]);2547+2548+ if(pcount > 0 && params[0] > 1.0) params[0] = 1.0 - params[0];2549+ if(pcount > 1 && params[1] > 1.0) params[1] = 1.0 - params[1];2550+ if(pcount > 2 && params[2] > 0.5*M_PI) params[2] = 0.5*M_PI - params[2];2551+}2552+2553+// helper function for getting the best fitting crop area;2554+// returns the negative area of the largest rectangle that fits within the2555+// defined image with a given rectangle's center and its aspect angle;2556+// the trick: the rectangle center coordinates are given in the input2557+// image coordinates so we know for sure that it also lies within the image after2558+// conversion to the output coordinates2559+static double crop_fitness(double *params, void *data)2560+{2561+ const dt_iop_ashift_cropfit_params_t *cropfit = (dt_iop_ashift_cropfit_params_t *)data;2562+2563+ const float wd = cropfit->width;2564+ const float ht = cropfit->height;2565+2566+ // get variable and constant parameters, respectively2567+ const float x = dt_isnan(cropfit->x) ? params[0] : cropfit->x;2568+ const float y = dt_isnan(cropfit->y) ? params[1] : cropfit->y;2569+ const float alpha = dt_isnan(cropfit->alpha) ? params[2] : cropfit->alpha;2570+2571+ // the center of the rectangle in input image coordinates2572+ const float DT_ALIGNED_PIXEL Pc[3] = { x * wd, y * ht, 1.0f };2573+2574+ // convert to the output image coordinates and normalize2575+ float DT_ALIGNED_PIXEL P[3];2576+ mat3mulv(P, (float *)cropfit->homograph, Pc);2577+ P[0] /= P[2];2578+ P[1] /= P[2];2579+ P[2] = 1.0f;2580+2581+ // two auxiliary points (some arbitrary distance away from P) to construct the diagonals2582+ const float DT_ALIGNED_ARRAY Pa[2][3] =2583+ { { P[0] + 10.0f * cosf(alpha), P[1] + 10.0f * sinf(alpha), 1.0f },2584+ { P[0] + 10.0f * cosf(alpha), P[1] - 10.0f * sinf(alpha), 1.0f } };2585+2586+ // the two diagonals: D = P x Pa2587+ float D[2][3];2588+ vec3prodn(D[0], P, Pa[0]);2589+ vec3prodn(D[1], P, Pa[1]);2590+2591+ // find all intersection points of all four edges with both2592+ // diagonals (I = E x D); the shortest distance d2min of the2593+ // intersection point I to the crop area center P determines the2594+ // size of the crop area that still fits into the image (for the2595+ // given center and aspect angle)2596+ float d2min = FLT_MAX;2597+ for(int k = 0; k < 4; k++)2598+ for(int l = 0; l < 2; l++)2599+ {2600+ // the intersection point2601+ float I[3];2602+ vec3prodn(I, cropfit->edges[k], D[l]);2603+2604+ // special case: I is all null -> E and D are identical -> P2605+ // lies on E -> d2min = 02606+ if(vec3isnull(I))2607+ {2608+ d2min = 0.0f;2609+ break;2610+ }2611+2612+ // special case: I[2] is 0.0f -> E and D are parallel and2613+ // intersect at infinity -> no relevant point2614+ if(I[2] == 0.0f)2615+ continue;2616+2617+ // the default case -> normalize I2618+ I[0] /= I[2];2619+ I[1] /= I[2];2620+2621+ // calculate distance from I to P2622+ const float d2 = sqrf(P[0] - I[0]) + sqrf(P[1] - I[1]);2623+2624+ // the minimum distance over all intersection points2625+ d2min = MIN(d2min, d2);2626+ }2627+2628+ // calculate the area of the rectangle2629+ const float A = 2.0f * d2min * sinf(2.0f * alpha);2630+2631+#ifdef ASHIFT_DEBUG2632+ printf("crop fitness with x %f, y %f, angle %f -> distance %f, area %f\n",2633+ x, y, alpha, d2min, A);2634+#endif2635+ // and return -A to allow Nelder-Mead simplex to search for the minimum2636+ return -A;2637+}2638+2639+// strategy: for a given center of the crop area and a specific aspect2640+// angle we calculate the largest crop area that still lies within the2641+// output image; now we allow a Nelder-Mead simplex to search for the2642+// center coordinates (and optionally the aspect angle) that delivers2643+// the largest overall crop area.2644+2645+static void do_crop(const dt_iop_module_t *self, dt_iop_ashift_params_t *p)2646+{2647+ dt_iop_ashift_gui_data_t *g = self->gui_data;2648+2649+ // if sizes are not ready (module disabled), just ignore this2650+ if(g->buf_width == 0 || g->buf_height == 0) return;2651+2652+ // skip if fitting is still running2653+ if(g->fitting) return;2654+2655+ // reset fit margins if auto-cropping is off2656+ if(p->cropmode == ASHIFT_CROP_OFF)2657+ {2658+ _clear_shadow_crop_box(g);2659+ _commit_crop_box(p, g);2660+ return;2661+ }2662+2663+ g->fitting = 1;2664+2665+ double params[3];2666+ int pcount;2667+2668+ // get parameters for the homograph2669+ const float f_length_kb = (p->mode == ASHIFT_MODE_GENERIC)2670+ ? DEFAULT_F_LENGTH2671+ : p->f_length * p->crop_factor;2672+2673+ const float orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr;2674+ const float aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect;2675+ const float rotation = p->rotation;2676+ const float lensshift_v = p->lensshift_v;2677+ const float lensshift_h = p->lensshift_h;2678+ const float shear = p->shear;2679+2680+ // prepare structure of constant parameters2681+ dt_iop_ashift_cropfit_params_t DT_ALIGNED_ARRAY cropfit;2682+ cropfit.width = g->buf_width;2683+ cropfit.height = g->buf_height;2684+ _homography((float *)cropfit.homograph, rotation, lensshift_v, lensshift_h,2685+ shear, f_length_kb,2686+ orthocorr, aspect, cropfit.width, cropfit.height, ASHIFT_HOMOGRAPH_FORWARD);2687+2688+ const float wd = cropfit.width;2689+ const float ht = cropfit.height;2690+2691+ // the four vertices of the image in input image coordinates2692+ const float Vc[4][3] = { { 0.0f, 0.0f, 1.0f },2693+ { 0.0f, ht, 1.0f },2694+ { wd, ht, 1.0f },2695+ { wd, 0.0f, 1.0f } };2696+2697+ // convert the vertices to output image coordinates2698+ float V[4][3];2699+ for(int n = 0; n < 4; n++)2700+ mat3mulv(V[n], (float *)cropfit.homograph, Vc[n]);2701+2702+ // get width and height of output image for later use2703+ float xmin = FLT_MAX, ymin = FLT_MAX, xmax = FLT_MIN, ymax = FLT_MIN;2704+ for(int n = 0; n < 4; n++)2705+ {2706+ // normalize V2707+ V[n][0] /= V[n][2];2708+ V[n][1] /= V[n][2];2709+ V[n][2] = 1.0f;2710+ xmin = MIN(xmin, V[n][0]);2711+ xmax = MAX(xmax, V[n][0]);2712+ ymin = MIN(ymin, V[n][1]);2713+ ymax = MAX(ymax, V[n][1]);2714+ }2715+ const float owd = xmax - xmin;2716+ const float oht = ymax - ymin;2717+2718+ // calculate the lines defining the four edges of the image area: E = V[n] x V[n+1]2719+ for(int n = 0; n < 4; n++)2720+ vec3prodn(cropfit.edges[n], V[n], V[(n + 1) % 4]);2721+2722+ // initial fit parameters: crop area is centered and aspect angle is2723+ // that of the original image number of parameters: fit only crop2724+ // center coordinates with a fixed aspect ratio, or fit all three2725+ // variables2726+ if(p->cropmode == ASHIFT_CROP_LARGEST)2727+ {2728+ params[0] = 0.5;2729+ params[1] = 0.5;2730+ params[2] = atan2f((float)cropfit.height, (float)cropfit.width);2731+ cropfit.x = NAN;2732+ cropfit.y = NAN;2733+ cropfit.alpha = NAN;2734+ pcount = 3;2735+ }2736+ else //(p->cropmode == ASHIFT_CROP_ASPECT)2737+ {2738+ params[0] = 0.5;2739+ params[1] = 0.5;2740+ cropfit.x = NAN;2741+ cropfit.y = NAN;2742+ cropfit.alpha = atan2f((float)cropfit.height, (float)cropfit.width);2743+ pcount = 2;2744+ }2745+2746+ // start the simplex fit2747+ const int iter = simplex(crop_fitness, params, pcount,2748+ NMS_CROP_EPSILON, NMS_CROP_SCALE, NMS_CROP_ITERATIONS,2749+ crop_constraint, (void*)&cropfit);2750+ // in case the fit did not converge -> failed2751+ if(iter >= NMS_CROP_ITERATIONS) goto failed;2752+2753+ // the fit did converge -> get clipping margins out of params:2754+ cropfit.x = dt_isnan(cropfit.x) ? params[0] : cropfit.x;2755+ cropfit.y = dt_isnan(cropfit.y) ? params[1] : cropfit.y;2756+ cropfit.alpha = dt_isnan(cropfit.alpha) ? params[2] : cropfit.alpha;2757+2758+ // the area of the best fitting rectangle2759+ const float A = fabs(crop_fitness(params, (void*)&cropfit));2760+2761+ // unlikely to happen but we need to catch this case2762+ if(A == 0.0f) goto failed;2763+2764+ // we need the half diagonal of that rectangle (this is in output2765+ // image dimensions); no need to check for division by zero here as2766+ // this case implies A == 0.0f, caught above2767+ const float d = sqrtf(A / (2.0f * sinf(2.0f * cropfit.alpha)));2768+2769+ // the rectangle's center in input image (homogeneous) coordinates2770+ const float Pc[3] = { cropfit.x * wd, cropfit.y * ht, 1.0f };2771+2772+ // convert rectangle center to output image coordinates and normalize2773+ float DT_ALIGNED_PIXEL P[3];2774+ mat3mulv(P, (float *)cropfit.homograph, Pc);2775+ P[0] /= P[2];2776+ P[1] /= P[2];2777+2778+ // calculate clipping margins relative to output image dimensions2779+ g->cl = CLAMP((P[0] - d * cosf(cropfit.alpha)) / owd, 0.0f, 1.0f);2780+ g->cr = CLAMP((P[0] + d * cosf(cropfit.alpha)) / owd, 0.0f, 1.0f);2781+ g->ct = CLAMP((P[1] - d * sinf(cropfit.alpha)) / oht, 0.0f, 1.0f);2782+ g->cb = CLAMP((P[1] + d * sinf(cropfit.alpha)) / oht, 0.0f, 1.0f);2783+2784+ // final sanity check2785+ if(g->cr - g->cl <= 0.0f || g->cb - g->ct <= 0.0f) goto failed;2786+2787+ g->fitting = 0;2788+2789+ dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE,2790+ "margins after crop fitting: iter=%d x=%.4f y=%.4f angle=%.4f"2791+ " crop area (%.4f %.4f %.4f %.4f) wd=%i ht=%i",2792+ iter, cropfit.x, cropfit.y, cropfit.alpha,2793+ g->cl, g->cr, g->ct, g->cb, cropfit.width, cropfit.height);2794+ dt_control_queue_redraw_center();2795+ return;2796+2797+failed:2798+ // in case of failure: reset clipping margins, set "automatic cropping" parameter2799+ // to "off" state, and display warning message2800+ _clear_shadow_crop_box(g);2801+ _commit_crop_box(p, g);2802+ p->cropmode = ASHIFT_CROP_OFF;2803+ dt_bauhaus_combobox_set(g->cropmode, p->cropmode);2804+ g->fitting = 0;2805+ dt_control_log(_("automatic cropping failed"));2806+ return;2807+}2808+2809+// manually adjust crop area by shifting its center2810+static void crop_adjust(const dt_iop_module_t *self,2811+ const dt_iop_ashift_params_t *const p,2812+ const float newx,2813+ const float newy)2814+{2815+ dt_iop_ashift_gui_data_t *g = self->gui_data;2816+2817+ // skip if fitting is still running2818+ if(g->fitting) return;2819+2820+ // get parameters for the homograph2821+ const float f_length_kb = (p->mode == ASHIFT_MODE_GENERIC)2822+ ? DEFAULT_F_LENGTH2823+ : p->f_length * p->crop_factor;2824+2825+ const float orthocorr = (p->mode == ASHIFT_MODE_GENERIC) ? 0.0f : p->orthocorr;2826+ const float aspect = (p->mode == ASHIFT_MODE_GENERIC) ? 1.0f : p->aspect;2827+ const float rotation = p->rotation;2828+ const float lensshift_v = p->lensshift_v;2829+ const float lensshift_h = p->lensshift_h;2830+ const float shear = p->shear;2831+ const float wd = g->buf_width;2832+ const float ht = g->buf_height;2833+2834+ const float alpha = atan2f(ht, wd);2835+2836+ float DT_ALIGNED_ARRAY homograph[3][3];2837+ _homography((float *)homograph, rotation, lensshift_v, lensshift_h, shear, f_length_kb,2838+ orthocorr, aspect, wd, ht, ASHIFT_HOMOGRAPH_FORWARD);2839+2840+ // the four vertices of the image in input image coordinates2841+ const float Vc[4][3] = { { 0.0f, 0.0f, 1.0f },2842+ { 0.0f, ht, 1.0f },2843+ { wd, ht, 1.0f },2844+ { wd, 0.0f, 1.0f } };2845+2846+ // convert the vertices to output image coordinates2847+ float V[4][3];2848+ for(int n = 0; n < 4; n++)2849+ mat3mulv(V[n], (float *)homograph, Vc[n]);2850+2851+ // get width and height of output image2852+ float xmin = FLT_MAX, ymin = FLT_MAX, xmax = FLT_MIN, ymax = FLT_MIN;2853+ for(int n = 0; n < 4; n++)2854+ {2855+ // normalize V2856+ V[n][0] /= V[n][2];2857+ V[n][1] /= V[n][2];2858+ V[n][2] = 1.0f;2859+ xmin = MIN(xmin, V[n][0]);2860+ xmax = MAX(xmax, V[n][0]);2861+ ymin = MIN(ymin, V[n][1]);2862+ ymax = MAX(ymax, V[n][1]);2863+ }2864+ const float owd = xmax - xmin;2865+ const float oht = ymax - ymin;2866+2867+ // calculate the lines defining the four edges of the image area: E = V[n] x V[n+1]2868+ float E[4][3];2869+ for(int n = 0; n < 4; n++)2870+ vec3prodn(E[n], V[n], V[(n + 1) % 4]);2871+2872+ // the center of the rectangle in output image coordinates2873+ const float DT_ALIGNED_PIXEL P[3] = { newx * owd, newy * oht, 1.0f };2874+2875+ // two auxiliary points (some arbitrary distance away from P) to2876+ // construct the diagonals2877+ const float Pa[2][3] =2878+ { { P[0] + 10.0f * cosf(alpha), P[1] + 10.0f * sinf(alpha), 1.0f },2879+ { P[0] + 10.0f * cosf(alpha), P[1] - 10.0f * sinf(alpha), 1.0f } };2880+2881+ // the two diagonals: D = P x Pa2882+ float D[2][3];2883+ vec3prodn(D[0], P, Pa[0]);2884+ vec3prodn(D[1], P, Pa[1]);2885+2886+ // find all intersection points of all four edges with both2887+ // diagonals (I = E x D); the shortest distance d2min of the2888+ // intersection point I to the crop area center P determines the2889+ // size of the crop area that still fits into the image (for the2890+ // given center and aspect angle)2891+ float d2min = FLT_MAX;2892+ for(int k = 0; k < 4; k++)2893+ for(int l = 0; l < 2; l++)2894+ {2895+ // the intersection point2896+ float I[3];2897+ vec3prodn(I, E[k], D[l]);2898+2899+ // special case: I is all null -> E and D are identical -> P lies on E -> d2min = 02900+ if(vec3isnull(I))2901+ {2902+ d2min = 0.0f;2903+ break;2904+ }2905+2906+ // special case: I[2] is 0.0f -> E and D are parallel and2907+ // intersect at infinity -> no relevant point2908+ if(I[2] == 0.0f)2909+ continue;2910+2911+ // the default case -> normalize I2912+ I[0] /= I[2];2913+ I[1] /= I[2];2914+2915+ // calculate distance from I to P2916+ const float d2 = sqrf(P[0] - I[0]) + sqrf(P[1] - I[1]);2917+2918+ // the minimum distance over all intersection points2919+ d2min = MIN(d2min, d2);2920+ }2921+2922+ const float d = sqrtf(d2min);2923+2924+ // do not allow crop area to drop below 1% of input image area2925+ const float A = 2.0f * d * d * sinf(2.0f * alpha);2926+ if(A < 0.01f * wd * ht) return;2927+2928+ // calculate clipping margins relative to output image dimensions2929+ g->cl = CLAMP((P[0] - d * cosf(alpha)) / owd, 0.0f, 1.0f);2930+ g->cr = CLAMP((P[0] + d * cosf(alpha)) / owd, 0.0f, 1.0f);2931+ g->ct = CLAMP((P[1] - d * sinf(alpha)) / oht, 0.0f, 1.0f);2932+ g->cb = CLAMP((P[1] + d * sinf(alpha)) / oht, 0.0f, 1.0f);2933+2934+ dt_print(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE,2935+ "margins after crop adjustment: x=%.3f y=%.3f angle=%.3f"2936+ " crop area (%.3f %.3f %.3f %.3f) width=%i height=%i",2937+ 0.5f * (g->cl + g->cr), 0.5f * (g->ct + g->cb), alpha,2938+ g->cl, g->cr, g->ct, g->cb, (int)wd, (int)ht);2939+ return;2940+}2941+2942+// determine if the line is vertical or horizontal2943+static void _draw_retrieve_line_type(dt_iop_ashift_line_t *line)2944+{2945+ dt_iop_ashift_linetype_t linetype = ASHIFT_LINE_VERTICAL_SELECTED;2946+ if(fabsf(line->p1[0] - line->p2[0]) > fabsf(line->p1[1] - line->p2[1]))2947+ linetype = ASHIFT_LINE_HORIZONTAL_SELECTED;2948+ line->type = linetype;2949+}2950+2951+// add a basic line. used for drawing perspective method2952+static void _draw_basic_line(dt_iop_ashift_line_t *line,2953+ const float x1,2954+ const float y1,2955+ const float x2,2956+ const float y2,2957+ const dt_iop_ashift_linetype_t type)2958+{2959+ // store as homogeneous coordinates2960+ line->p1[0] = x1;2961+ line->p1[1] = y1;2962+ line->p1[2] = 1.0f;2963+ line->p2[0] = x2;2964+ line->p2[1] = y2;2965+ line->p2[2] = 1.0f;2966+2967+ // calculate homogeneous coordinates of connecting line (defined by the two points)2968+ vec3prodn(line->L, line->p1, line->p2);2969+2970+ // normalaze line coordinates so that x^2 + y^2 = 12971+ // (this will always succeed as L is a real line connecting two real points)2972+ vec3lnorm(line->L, line->L);2973+2974+ // length and width of rectangle (see LSD)2975+ line->length = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));2976+ line->width = 1.0f;2977+ line->weight = 1.0f;2978+2979+ // register type of line2980+ line->type = type;2981+}2982+2983+static void _gui_update_structure_states(const dt_iop_module_t *self, GtkWidget *widget)2984+{2985+ const dt_iop_ashift_gui_data_t *g = self->gui_data;2986+ if(widget && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))2987+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);2988+ else2989+ {2990+ if(widget != g->structure_lines)2991+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_lines), FALSE);2992+ if(widget != g->structure_quad)2993+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_quad), FALSE);2994+ if(widget != g->structure_auto)2995+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->structure_auto), FALSE);2996+ if(widget) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), TRUE);2997+ }2998+2999+ // update fit buttons state3000+ const gboolean enable =
diff truncated
A native/upstream/ashift_lsd.c +2283 -0

Not shown: this page already shows 6000 lines of diff.

A native/upstream/ashift_nmsimplex.c +420 -0

Not shown: this page already shows 6000 lines of diff.

A perspective.html +97 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/build.sh +10 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/extract.py +48 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/icons.py +16 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/package.py +17 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/serve.py +23 -0

Not shown: this page already shows 6000 lines of diff.

A scripts/test-core.mjs +51 -0

Not shown: this page already shows 6000 lines of diff.

A style.css +301 -0

Not shown: this page already shows 6000 lines of diff.

A sw.js +17 -0

Not shown: this page already shows 6000 lines of diff.

A worker.mjs +64 -0

Not shown: this page already shows 6000 lines of diff.

A wrangler.jsonc +6 -0

Not shown: this page already shows 6000 lines of diff.