├── .gitignore ├── LICENSE ├── README.md ├── calibration.py ├── cloud_detection.py ├── configuration.py ├── control.py ├── frame_difference.py ├── image_view.ui ├── main.py ├── main.ui ├── moon.py ├── night.py ├── settings_view.ui ├── sky.py ├── skycamera.py ├── skycamerafile.py ├── star_checker.py └── star_detection.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | 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 to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | 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 License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | 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 of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 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 of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | 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 visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | 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 work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | 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 all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | 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 not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | 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 technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | 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 all 203 | 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 to 211 | produce it from the Program, in the form of source code under the 212 | 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 modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | 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 an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | 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 terms 248 | of sections 4 and 5, provided that you also convey the 249 | 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 product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | 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 install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | 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 publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | 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 you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | 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 expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | 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 or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | 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 automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | 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 this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | 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 anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | 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 limiting 537 | any implied license or other defenses to infringement that may 538 | 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 or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | 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 have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | 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 the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | 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 WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | 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 OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | 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 safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | 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 modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (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 of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 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 short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | 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, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pynephoscope 2 | A python software suite for cloud detection in all-sky camera images. 3 | 4 | ## requirements 5 | 6 | - PyQt 7 | - astropy 8 | - pyephem 9 | - scipy 10 | - numpy 11 | - pandas 12 | - OpenCV 13 | - pyserial 14 | 15 | ## run 16 | 17 | - you need a mask and a star catalog 18 | - edit the configuration in configuration.py 19 | - run pyuic on the ui files 20 | - run main.py for the UI or any of the other scripts 21 | -------------------------------------------------------------------------------- /calibration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import sys 22 | import os 23 | import cv2 24 | import pickle 25 | import numpy as np 26 | 27 | from scipy.optimize import minimize 28 | 29 | from skycamerafile import SkyCameraFile 30 | from sky import SkyCatalog, SkyRenderer 31 | from astropy.coordinates import EarthLocation, AltAz 32 | from astropy import units as u 33 | from configuration import Configuration 34 | 35 | class StarCorrespondence: 36 | def __init__(self, pos, altaz): 37 | self.pos = pos 38 | self.altaz = altaz 39 | 40 | class Projector: 41 | def project(self, altaz, k): 42 | phi = altaz[:, 1] + k[2] 43 | 44 | cpsi = np.cos(altaz[:, 0]) 45 | spsi = np.sin(altaz[:, 0]) 46 | cphi = np.cos(phi) 47 | sphi = np.sin(phi) 48 | 49 | ck4 = np.cos(k[3]) 50 | sk4 = np.sin(k[3]) 51 | 52 | rho_nom = ck4 * cpsi * sphi - sk4 * spsi 53 | rho_den = cpsi * cphi 54 | rho = np.arctan2(rho_nom, rho_den) 55 | 56 | crho = np.cos(rho) 57 | 58 | tau_nom = sk4 * cpsi * sphi + ck4 * spsi 59 | tau_den = np.sqrt((cpsi * cphi)**2 + (ck4*cpsi*sphi - sk4*spsi)**2) 60 | tau = np.arctan2(tau_nom, tau_den) + k[4] 61 | 62 | theta = np.pi / 2 - tau 63 | 64 | # fisheye projection with a sine (James Kumler and Martin Bauer): 65 | #r = k[0] * np.sin(k[1] * theta) 66 | # fisheye projection with a nice universal model (Donald Gennery): 67 | #r = k[0] * np.sin(k[1] * theta) / np.cos(np.maximum(0, k[1] * theta)) 68 | # Rectilinear aka Perspective 69 | #r = k[0] * np.tan(k[1] * theta) 70 | # Equidistant aka equi-angular 71 | #r = k[0] * k[1] * theta 72 | # Quadratic 73 | #r = k[0] * theta + k[1] * theta**2 74 | # Cubic 75 | r = k[0] * theta + k[1] * theta**3 76 | # Fish-Eye Transform (FET, Basu and Licardie) 77 | #r = k[0] * np.log(1 + k[1] * np.tan(theta)) 78 | # Equisolid 79 | #r = k[0] * k[1] * np.sin(theta/2) 80 | # Orthographic 81 | #r = k[0] * k[1] * np.sin(theta) 82 | 83 | crho = np.cos(- np.pi / 2 - rho) 84 | srho = np.sin(- np.pi / 2 - rho) 85 | 86 | a = r * crho + k[5] 87 | b = r * srho + k[6] 88 | 89 | return a, b 90 | 91 | def unproject(self, pos, k): 92 | rcrho = pos[0] - k[5] 93 | rsrho = pos[1] - k[6] 94 | 95 | rho = -np.pi / 2 - np.arctan2(rsrho, rcrho) 96 | 97 | r = np.sqrt(rcrho ** 2 + rsrho ** 2) 98 | 99 | # Polynomial 100 | theta = abs((-k[0] - np.sqrt(k[0] * k[0] - 4 * k[1] * (-r))) / (2 * k[1])) 101 | tau = np.pi / 2 - theta - k[4] 102 | 103 | ctau = np.cos(tau) 104 | stau = np.sin(tau) 105 | crho = np.cos(rho) 106 | srho = np.sin(rho) 107 | 108 | ck4 = np.cos(k[3]) 109 | sk4 = -np.sin(k[3]) 110 | 111 | # az 112 | phi_nom = ck4 * ctau * srho - sk4 * stau 113 | phi_den = ctau * crho 114 | phi = np.arctan2(phi_nom, phi_den) - k[2] 115 | 116 | if phi < 0: 117 | phi += 2 * np.pi 118 | 119 | # alt 120 | psi_nom = sk4 * ctau * srho + ck4 * stau 121 | psi_den = np.sqrt((ctau * crho)**2 + (ck4*ctau*srho - sk4*stau)**2) 122 | psi = np.arctan2(psi_nom, psi_den) 123 | 124 | return psi, phi 125 | 126 | class Calibration: 127 | def __init__(self, location = None, catalog = None): 128 | self.coeff = None 129 | 130 | self.projector = Projector() 131 | 132 | if catalog is None: 133 | self.catalog = SkyCatalog() 134 | else: 135 | self.catalog = catalog 136 | 137 | if location is None: 138 | location = EarthLocation(lat=Configuration.latitude, lon=Configuration.longitude, height=Configuration.elevation) 139 | 140 | self.catalog.setLocation(location) 141 | 142 | def selectImage(self, filename): 143 | time = SkyCameraFile.parseTime(filename) 144 | self.catalog.setTime(time) 145 | self.catalog.calculate() 146 | 147 | def save(self, calibration_file=None): 148 | if calibration_file is None: 149 | calibration_file = Configuration.calibration_file 150 | 151 | with open(calibration_file, 'wb') as f: 152 | pickle.dump(self.coeff, f) 153 | 154 | def load(self, calibration_file=None): 155 | if calibration_file is None: 156 | calibration_file = Configuration.calibration_file 157 | 158 | with open(calibration_file, 'rb') as f: 159 | self.coeff = pickle.load(f) 160 | 161 | def project(self, pos=None, k=None): 162 | if pos is None: 163 | pos = np.array([self.catalog.alt.radian, self.catalog.az.radian]).transpose() 164 | 165 | if k is None: 166 | k = self.coeff 167 | 168 | return self.projector.project(pos, k) 169 | 170 | def unproject(self, pos, k=None): 171 | if k is None: 172 | k = self.coeff 173 | 174 | return self.projector.unproject(pos, k) 175 | 176 | class Calibrator: 177 | def __init__(self, files, location = None, catalog = None): 178 | if files == None: 179 | files = [] 180 | 181 | self.files = files 182 | 183 | if len(self.files) > 0: 184 | self.current_file = SkyCameraFile.uniqueName(files[0]) 185 | self.correspondences = {self.current_file: []} 186 | else: 187 | self.current_file = None 188 | self.correspondences = {} 189 | 190 | self.min_max_range = 5 191 | self.orders = 1 192 | self.nonlinear = True 193 | self.parameter_set = 0 194 | self.image = None 195 | 196 | self.calibration = Calibration(location, catalog) 197 | 198 | def addImage(self, filename): 199 | self.files.append(filename) 200 | return len(self.files) - 1 201 | 202 | def selectImage(self, number, load=True): 203 | filename = self.files[number] 204 | self.current_file = SkyCameraFile.uniqueName(filename) 205 | 206 | if self.current_file not in self.correspondences: 207 | self.correspondences[self.current_file] = [] 208 | 209 | if load: 210 | self.image = cv2.imread(filename) 211 | 212 | self.calibration.selectImage(filename) 213 | 214 | def findImageStar(self, x, y): 215 | x -= self.min_max_range 216 | y -= self.min_max_range 217 | roi = self.image[y:y + 2 * self.min_max_range + 1, x:x + 2 * self.min_max_range + 1] 218 | roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) 219 | _, _, _, maxLoc = cv2.minMaxLoc(roi) 220 | x += maxLoc[0] 221 | y += maxLoc[1] 222 | 223 | return x, y 224 | 225 | def getCurrentCorrespondences(self): 226 | return self.correspondences[self.current_file] 227 | 228 | def addCorrespondence(self, pos, altaz): 229 | self.correspondences[self.current_file].append(StarCorrespondence(pos, altaz)) 230 | return len(self.correspondences[self.current_file]) - 1 231 | 232 | def removeCorrespondence(self, index): 233 | del(self.correspondences[self.current_file][index]) 234 | 235 | def findAltAzCorrespondence(self, altaz): 236 | for index, correspondence in enumerate(self.correspondences[self.current_file]): 237 | if correspondence.altaz == altaz: 238 | return index 239 | 240 | return None 241 | 242 | def setCorrespondencePos(self, index, pos): 243 | if index is None: 244 | return False 245 | 246 | if self.correspondences[self.current_file][index].pos is None: 247 | self.correspondences[self.current_file][index].pos = pos 248 | return True 249 | 250 | return False 251 | 252 | def setCorrespondenceAltaz(self, index, altaz): 253 | if index is None: 254 | return False 255 | 256 | if self.correspondences[self.current_file][index].altaz is None: 257 | self.correspondences[self.current_file][index].altaz = altaz 258 | return True 259 | 260 | return False 261 | 262 | def findEmptyPos(self): 263 | for index, correspondence in enumerate(reversed(self.correspondences[self.current_file])): 264 | if correspondence.pos is None: 265 | return len(self.correspondences[self.current_file]) - index - 1 266 | 267 | return None 268 | 269 | def findEmptyAltAz(self): 270 | for index, correspondence in enumerate(reversed(self.correspondences[self.current_file])): 271 | if correspondence.altaz is None: 272 | return len(self.correspondences[self.current_file]) - index - 1 273 | 274 | return None 275 | 276 | def save(self, calibration_file=None, correspondence_file=None): 277 | self.calibration.save(calibration_file) 278 | 279 | if correspondence_file is None: 280 | correspondence_file = Configuration.correspondence_file 281 | 282 | with open(correspondence_file, 'wb') as f: 283 | pickle.dump(self.correspondences, f) 284 | 285 | def load(self, calibration_file=None, correspondence_file=None): 286 | self.calibration.load(calibration_file) 287 | 288 | if correspondence_file is None: 289 | correspondence_file = Configuration.correspondence_file 290 | 291 | with open(correspondence_file, 'rb') as f: 292 | self.correspondences = pickle.load(f) 293 | 294 | if self.current_file not in self.correspondences: 295 | self.correspondences[self.current_file] = [] 296 | 297 | def resetCurrent(self): 298 | self.correspondences[self.current_file] = [] 299 | 300 | def altazToInput(self, altaz): 301 | r = 1 - altaz[:, 0] / (np.pi / 2) 302 | 303 | shape = altaz.shape 304 | 305 | if shape[1] != 2 or len(shape) > 2: 306 | raise Exception('Invalid input data for transform') 307 | 308 | if self.parameter_set == 0: 309 | shape = (shape[0], 1 + 2 * self.orders) 310 | 311 | inpos = np.ones(shape) 312 | 313 | inpos[:, 0] = r * np.cos(-np.pi / 2 - altaz[:, 1]) 314 | inpos[:, 1] = r * np.sin(-np.pi / 2 - altaz[:, 1]) 315 | 316 | for i in range(1, self.orders): 317 | inpos[:, 2 * i + 0] = inpos[:, 0] ** (i + 1) 318 | inpos[:, 2 * i + 1] = inpos[:, 1] ** (i + 1) 319 | elif self.parameter_set == 1: 320 | shape = (shape[0], 1 + 4 * self.orders) 321 | 322 | inpos = np.ones(shape) 323 | 324 | inpos[:, 0] = r * np.cos(-np.pi / 2 - altaz[:, 1]) 325 | inpos[:, 1] = r * np.sin(-np.pi / 2 - altaz[:, 1]) 326 | inpos[:, 2 * (self.orders) + 0] = altaz[:, 0] 327 | inpos[:, 2 * (self.orders) + 1] = altaz[:, 1] 328 | 329 | for i in range(1, self.orders): 330 | inpos[:, 2 * i + 0] = inpos[:, 0] ** (i + 1) 331 | inpos[:, 2 * i + 1] = inpos[:, 1] ** (i + 1) 332 | inpos[:, 2 * (self.orders + i) + 0] = altaz[:, 0] ** (i + 1) 333 | inpos[:, 2 * (self.orders + i) + 1] = altaz[:, 1] ** (i + 1) 334 | else: 335 | shape = (shape[0], 3 + self.orders) 336 | 337 | inpos = np.ones(shape) 338 | 339 | inpos[:, 0] = r * np.cos(-np.pi / 2 - altaz[:, 1]) 340 | inpos[:, 1] = r * np.sin(-np.pi / 2 - altaz[:, 1]) 341 | 342 | for i in range(0, self.orders): 343 | inpos[:, i + 2] = r ** (i + 1) 344 | 345 | return inpos 346 | 347 | def gatherData(self): 348 | correspondences = [] 349 | for _, c in self.correspondences.items(): 350 | for correspondence in c: 351 | if correspondence.pos is not None and correspondence.altaz is not None: 352 | correspondences.append(correspondence) 353 | 354 | count = len(correspondences) 355 | 356 | altaz = np.zeros((count, 2)) 357 | pos = np.zeros((count, 2)) 358 | 359 | for index, correspondence in enumerate(correspondences): 360 | pos[index, :] = correspondence.pos 361 | altaz[index, :] = [angle.radian for angle in correspondence.altaz] 362 | 363 | return pos, altaz 364 | 365 | def calibrateExt(self): 366 | self.pos, self.altaz = self.gatherData() 367 | 368 | k2 = 0.5 369 | k1 = 1 / np.sin(k2 * np.pi / 2) * 400 370 | k3 = 0 371 | k4 = k5 = k3 372 | k6 = 296.03261333 373 | k7 = 218.56917001 374 | 375 | k0 = [k1, k2, k3, k4, k5, k6, k7] 376 | 377 | res = minimize(self.errorFunction, k0, method='nelder-mead', options={'xtol': 1e-9, 'disp': False, 'maxfev': 1e5, 'maxiter': 1e5}) 378 | 379 | return res.x 380 | 381 | def errorFunction(self, k): 382 | a, b = self.calibration.project(self.altaz, k) 383 | 384 | x = self.pos[:, 0] 385 | y = self.pos[:, 1] 386 | 387 | f = np.mean((a - x)**2 + (b - y)**2) 388 | 389 | return f 390 | 391 | def lstsq(self): 392 | pos, altaz = self.gatherData() 393 | 394 | inpos = self.altazToInput(altaz) 395 | 396 | coeff, _, _, _ = np.linalg.lstsq(inpos, pos) 397 | 398 | return coeff 399 | 400 | def calibrate(self): 401 | if self.nonlinear: 402 | self.calibration.coeff = self.calibrateExt() 403 | else: 404 | self.calibration.coeff = self.lstsq() 405 | return self.calibration.coeff 406 | 407 | def transform(self, altaz): 408 | if not isinstance(altaz, np.ndarray): 409 | altaz = np.array([a.radian for a in altaz]) 410 | 411 | if len(altaz.shape) == 1: 412 | altaz = np.array([altaz]) 413 | 414 | if self.nonlinear: 415 | a, b = self.calibration.project(altaz, self.calibration.coeff) 416 | return np.column_stack((a, b)) 417 | else: 418 | inpos = self.altazToInput(altaz) 419 | 420 | pos = np.matrix(inpos) * np.matrix(self.calibration.coeff) 421 | 422 | return pos 423 | 424 | class CalibratorUI: 425 | def __init__(self): 426 | if len(sys.argv) < 2: 427 | print('Usage: calibration []') 428 | print('The supplied directory should contain the calibration images.') 429 | sys.exit(1) 430 | 431 | size = 640 432 | 433 | self.path = sys.argv[1] 434 | self.image_window = 'Image Calibration' 435 | self.sky_window = 'Sky Calibration' 436 | self.tb_image_switch = 'image' 437 | self.tb_max_mag = 'maximum magnitude' 438 | self.save_file_name = 'data' 439 | self.selected_star = None 440 | self.selected_color = (0, 0, 255) 441 | self.marked_color = (0, 255, 0) 442 | self.circle_radius = 5 443 | self.max_mag = 4 444 | self.renderer = SkyRenderer(size) 445 | 446 | try: 447 | self.calibrator = Calibrator(SkyCameraFile.glob(self.path), EarthLocation(lat=Configuration.latitude, lon=Configuration.longitude, height=Configuration.elevation)) 448 | except Exception as e: 449 | print(e.message) 450 | sys.exit(2) 451 | 452 | if len(sys.argv) > 2: 453 | self.save_file_name = sys.argv[2] 454 | if os.path.exists(self.save_file_name): 455 | self.calibrator.load(self.save_file_name) 456 | 457 | cv2.namedWindow(self.image_window, cv2.WINDOW_AUTOSIZE) 458 | cv2.namedWindow(self.sky_window, cv2.WINDOW_AUTOSIZE) 459 | 460 | self.selectImage(0) 461 | 462 | cv2.setMouseCallback(self.image_window, self.imageMouseCallback) 463 | cv2.setMouseCallback(self.sky_window, self.skyMouseCallback) 464 | cv2.createTrackbar(self.tb_image_switch, self.image_window, 0, len(self.calibrator.files) - 1, self.selectImage) 465 | cv2.createTrackbar(self.tb_max_mag, self.sky_window, self.max_mag, 6, self.setMaxMag) 466 | 467 | def selectImage(self, number): 468 | self.calibrator.selectImage(number) 469 | self.renderer.renderCatalog(self.calibrator.calibration.catalog, self.max_mag) 470 | self.selected_star = None 471 | self.render() 472 | 473 | def setMaxMag(self, mag): 474 | self.max_mag = mag 475 | self.renderer.renderCatalog(self.calibrator.calibration.catalog, self.max_mag) 476 | self.render() 477 | 478 | def render(self): 479 | image = self.calibrator.image.copy() 480 | sky = cv2.cvtColor(self.renderer.image.copy(), cv2.COLOR_GRAY2BGR) 481 | 482 | correspondences = self.calibrator.getCurrentCorrespondences() 483 | 484 | for correspondence in correspondences: 485 | if correspondence.pos is not None: 486 | cv2.circle(image, correspondence.pos, self.circle_radius, self.marked_color) 487 | if correspondence.altaz is not None: 488 | self.renderer.highlightStar(sky, correspondence.altaz, self.circle_radius, self.marked_color) 489 | 490 | if self.selected_star is not None: 491 | if correspondences[self.selected_star].pos is not None: 492 | cv2.circle(image, correspondences[self.selected_star].pos, self.circle_radius, self.selected_color) 493 | if correspondences[self.selected_star].altaz is not None: 494 | self.renderer.highlightStar(sky, correspondences[self.selected_star].altaz, self.circle_radius, self.selected_color) 495 | 496 | cv2.imshow(self.image_window, image) 497 | cv2.imshow(self.sky_window, sky) 498 | 499 | def renderCalibrationResult(self): 500 | num_circles = 9 501 | num_points = 64 502 | num = num_circles * num_points 503 | altaz = np.ones((num, 2)) 504 | 505 | for c in range(num_circles): 506 | alt = c / 18 * np.pi 507 | for p in range(num_points): 508 | az = p / num_points * 2 * np.pi 509 | index = c * num_points + p 510 | altaz[index, 0] = alt 511 | altaz[index, 1] = az 512 | 513 | pos = self.calibrator.transform(altaz) 514 | inpos = self.renderer.altazToPos(altaz) 515 | 516 | image = self.calibrator.image.copy() 517 | sky = cv2.cvtColor(self.renderer.image.copy(), cv2.COLOR_GRAY2BGR) 518 | 519 | for c in range(num_circles): 520 | pts = np.array(pos[c * num_points:(c + 1) * num_points, :], np.int32) 521 | pts = pts.reshape((-1,1,2)) 522 | cv2.polylines(image, [pts], True, (255, 0, 0)) 523 | 524 | pts = np.array(inpos[c * num_points:(c + 1) * num_points, 0:2], np.int32) 525 | pts = pts.reshape((-1,1,2)) 526 | cv2.polylines(sky, [pts], True, (255, 0, 0)) 527 | 528 | correspondences = self.calibrator.getCurrentCorrespondences() 529 | 530 | for correspondence in correspondences: 531 | if correspondence.pos is not None: 532 | cv2.circle(image, correspondence.pos, self.circle_radius, self.selected_color) 533 | if correspondence.altaz is not None: 534 | altaz = correspondence.altaz 535 | 536 | pos = np.array(self.calibrator.transform(altaz), np.int32)[0] # np.array([np.array([a.radian for a in altaz])]) TODO 537 | pos = (pos[0], pos[1]) 538 | 539 | cv2.circle(image, pos, self.circle_radius, self.marked_color) 540 | self.renderer.highlightStar(sky, correspondence.altaz, self.circle_radius, self.marked_color) 541 | 542 | cv2.imshow(self.image_window, image) 543 | cv2.imshow(self.sky_window, sky) 544 | 545 | def findCorrespondence(self, x, y): 546 | correspondences = self.calibrator.getCurrentCorrespondences() 547 | 548 | r2 = self.circle_radius * self.circle_radius 549 | 550 | for index, correspondence in enumerate(correspondences): 551 | if correspondence.pos is None: 552 | continue 553 | 554 | diff = np.subtract(correspondence.pos, (x, y)) 555 | 556 | if np.dot(diff, diff) <= r2: 557 | return index 558 | 559 | return None 560 | 561 | def deleteSelectedStar(self): 562 | if self.selected_star is not None: 563 | self.calibrator.removeCorrespondence(self.selected_star) 564 | self.selected_star = None 565 | self.render() 566 | 567 | def imageMouseCallback(self, event, x, y, flags, param): 568 | if event == cv2.EVENT_LBUTTONDOWN: 569 | x, y = self.calibrator.findImageStar(x, y) 570 | 571 | correspondence = self.findCorrespondence(x, y) 572 | 573 | if correspondence is None: 574 | if self.calibrator.setCorrespondencePos(self.selected_star, (x, y)): 575 | self.selected_star = self.calibrator.findEmptyPos() 576 | else: 577 | self.selected_star = self.calibrator.addCorrespondence((x, y), None) 578 | else: 579 | self.selected_star = correspondence 580 | 581 | self.render() 582 | 583 | elif event == cv2.EVENT_RBUTTONDOWN: 584 | self.deleteSelectedStar() 585 | 586 | def skyMouseCallback(self, event, x, y, flags, param): 587 | if event == cv2.EVENT_LBUTTONDOWN: 588 | res = self.renderer.findStar(x, y, self.circle_radius) 589 | 590 | if res is None: 591 | return 592 | 593 | altaz = (res[0], res[1]) 594 | correspondence = self.calibrator.findAltAzCorrespondence(altaz) 595 | 596 | if correspondence is None: 597 | if self.calibrator.setCorrespondenceAltaz(self.selected_star, altaz): 598 | self.selected_star = self.calibrator.findEmptyAltAz() 599 | else: 600 | self.selected_star = self.calibrator.addCorrespondence(None, altaz) 601 | else: 602 | self.selected_star = correspondence 603 | 604 | self.render() 605 | elif event == cv2.EVENT_RBUTTONDOWN: 606 | self.deleteSelectedStar() 607 | 608 | def run(self): 609 | quit = False 610 | 611 | while not quit: 612 | k = cv2.waitKey(0) & 0xFF 613 | if k == 27 or k == ord('q'): # ESC 614 | quit = True 615 | 616 | elif k == ord('c'): 617 | coeff = self.calibrator.calibrate() 618 | 619 | error = self.calibrator.errorFunction(coeff) 620 | 621 | print('Calibration result:', error) 622 | print(coeff) 623 | 624 | self.renderCalibrationResult() 625 | 626 | elif k == ord('d'): 627 | self.renderCalibrationResult() 628 | 629 | elif k == ord('s'): 630 | self.calibrator.save(self.save_file_name) 631 | print('Saved') 632 | 633 | elif k == ord('l'): 634 | self.calibrator.load(self.save_file_name) 635 | self.selected_star = None 636 | self.render() 637 | 638 | elif k == ord('r'): 639 | self.calibrator.resetCurrent() 640 | self.render() 641 | 642 | cv2.destroyAllWindows() 643 | 644 | if __name__ == '__main__': 645 | ui = CalibratorUI() 646 | ui.run() 647 | 648 | #__import__("code").interact(local=locals()) 649 | -------------------------------------------------------------------------------- /cloud_detection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import cv2 22 | import numpy as np 23 | import skycamera 24 | import pandas as pd 25 | import pickle 26 | from configuration import Configuration 27 | from skycamerafile import * 28 | from calibration import Calibration 29 | from sky import * 30 | 31 | class CDRBDifference: 32 | def detect(self, image, mask = None): 33 | b,g,r = cv2.split(image) 34 | 35 | difference = cv2.subtract(np.float32(r), np.float32(b)) 36 | 37 | _, result = cv2.threshold(difference, Configuration.rb_difference_threshold, 1, cv2.THRESH_BINARY) 38 | 39 | return np.uint8(result) 40 | 41 | class CDRBRatio: 42 | def detect(self, image, mask = None): 43 | floatimage = np.float32(image) 44 | 45 | fb,fg,fr = cv2.split(floatimage) 46 | 47 | nonzero = fb != 0 48 | difference = np.zeros(fr.shape, np.float32) 49 | difference[nonzero] = fr[nonzero] / fb[nonzero] 50 | _, result = cv2.threshold(difference, Configuration.rb_ratio_threshold, 1, cv2.THRESH_BINARY) 51 | return np.uint8(result) 52 | 53 | class CDBRRatio: 54 | def detect(self, image, mask = None): 55 | floatimage = np.float32(image) 56 | 57 | fb,fg,fr = cv2.split(floatimage) 58 | 59 | nonzero = fr != 0 60 | difference = np.zeros(fr.shape, np.float32) 61 | difference[nonzero] = fb[nonzero] / fr[nonzero] 62 | _, result = cv2.threshold(difference, Configuration.br_ratio_threshold, 1, cv2.THRESH_BINARY_INV) 63 | return np.uint8(result) 64 | 65 | class CDNBRRatio: 66 | def detect(self, image, mask = None): 67 | floatimage = np.float32(image) 68 | 69 | fb,fg,fr = cv2.split(floatimage) 70 | 71 | nonzero = (fr + fb) != 0 72 | difference = np.zeros(fr.shape, np.float32) 73 | difference[nonzero] = (fb[nonzero] - fr[nonzero]) / (fb[nonzero] + fr[nonzero]) 74 | _, result = cv2.threshold(difference, Configuration.nbr_threshold, 1, cv2.THRESH_BINARY_INV) 75 | return np.uint8(result) 76 | 77 | class CDAdaptive: 78 | def detect(self, image, mask = None): 79 | b,g,r = cv2.split(image) 80 | 81 | difference = cv2.subtract(r, b) 82 | difference = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 83 | 84 | # ADAPTIVE_THRESH_GAUSSIAN_C or ADAPTIVE_THRESH_MEAN_C 85 | return cv2.adaptiveThreshold(difference, 1, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, Configuration.adaptive_block_size, Configuration.adaptive_threshold) 86 | 87 | class CDBackground: 88 | def __init__(self): 89 | self.kernel = None 90 | 91 | def detect(self, image, mask = None): 92 | floatimage = np.float32(image) 93 | 94 | fb,fg,fr = cv2.split(floatimage) 95 | 96 | # red-to-blue channel operation 97 | ra = fr + fb 98 | rb = fr - fb 99 | rb[ra > 0] /= ra[ra > 0] 100 | #mi = np.min(rb) 101 | #ma = np.max(rb) 102 | #rb = np.uint8((rb - mi) / (ma - mi) * 255) 103 | 104 | # morphology open 105 | if self.kernel is None or self.kernel.shape[0] != Configuration.background_rect_size: 106 | self.kernel = np.ones((Configuration.background_rect_size, Configuration.background_rect_size), np.uint8) * 255 107 | 108 | result = cv2.morphologyEx(rb, cv2.MORPH_OPEN, self.kernel) 109 | 110 | # background subtraction 111 | # homogeneous background image V 112 | result = rb - result 113 | 114 | mi = np.min(result) 115 | ma = np.max(result) 116 | result = np.uint8((result - mi) / (ma - mi) * 255) 117 | 118 | # adaptive threshold T 119 | T, _ = cv2.threshold(result[mask == 0], 0, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU) 120 | 121 | # V(i, j) > T 122 | return np.uint8((T - np.float32(result)) <= 0) 123 | 124 | class CDMulticolor: 125 | def detect(self, image, mask = None): 126 | b,g,r = cv2.split(image) 127 | 128 | return np.uint8((b < r + Configuration.mc_rb_threshold) & (b < g + Configuration.mc_bg_threshold) & (b < Configuration.mc_b_threshold)) 129 | 130 | class CDSuperPixel: 131 | def __init__(self): 132 | self.width = None 133 | self.height = None 134 | self.seeds = None 135 | self.threshold = None 136 | 137 | self.reset() 138 | 139 | self.gaussian = None 140 | 141 | def reset(self): 142 | self.num_superpixels = Configuration.sp_num_superpixels 143 | self.prior = Configuration.sp_prior 144 | self.num_levels = Configuration.sp_num_levels 145 | self.num_histogram_bins = Configuration.sp_num_histogram_bins 146 | 147 | def detect(self, image, mask = None): 148 | if self.width != image.shape[1] or self.height != image.shape[0] or self.channels != image.shape[2]: 149 | self.seeds = None 150 | 151 | if self.num_superpixels != Configuration.sp_num_superpixels or self.prior != Configuration.sp_prior or self.num_levels != Configuration.sp_num_levels or self.num_histogram_bins != Configuration.sp_num_histogram_bins: 152 | self.seeds = None 153 | self.reset() 154 | 155 | if self.seeds is None: 156 | self.width = image.shape[1] 157 | self.height = image.shape[0] 158 | self.channels = image.shape[2] 159 | self.seeds = cv2.ximgproc.createSuperpixelSEEDS(self.width, self.height, self.channels, Configuration.sp_num_superpixels, Configuration.sp_num_levels, Configuration.sp_prior, Configuration.sp_num_histogram_bins) 160 | self.threshold = np.ones((self.height, self.width), np.float32) 161 | 162 | converted_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 163 | 164 | self.seeds.iterate(converted_img, Configuration.sp_num_iterations) 165 | labels = self.seeds.getLabels() 166 | 167 | if mask is None: 168 | mask = np.zeros((image.shape[0], image.shape[1]), np.uint8) 169 | 170 | floatimage = np.float32(image) 171 | fb, fg, fr = cv2.split(floatimage) 172 | 173 | rb = fr - fb 174 | #rb = fr + fb + fg 175 | mi = np.min(rb[mask == 0]) 176 | ma = np.max(rb[mask == 0]) 177 | rb = np.uint8((rb - mi) / (ma - mi) * 255) 178 | 179 | #mimaTg = np.uint8((np.array([-15, 15]) - mi) / (ma - mi) * 255) 180 | 181 | Tg, _ = cv2.threshold(rb[mask == 0], 0, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU) 182 | 183 | #if Tg < mimaTg[0]: 184 | # Tg = mimaTg[0] 185 | #elif Tg > mimaTg[1]: 186 | # Tg = mimaTg[1] 187 | 188 | Tl = np.zeros(self.seeds.getNumberOfSuperpixels()) 189 | 190 | for i in range(self.seeds.getNumberOfSuperpixels()): 191 | sp = rb[(labels == i) & (mask == 0)] 192 | if sp.size == 0: 193 | self.threshold[labels == i] = Tg 194 | continue 195 | 196 | Lmax = np.max(sp) 197 | Lmin = np.min(sp) 198 | if Lmax < Tg: 199 | Tl[i] = Tg#Lmax 200 | elif Lmin > Tg: 201 | Tl[i] = Tg#Lmin 202 | else: 203 | Sl, _ = cv2.threshold(sp, 0, 1, cv2.THRESH_BINARY | cv2.THRESH_OTSU) 204 | Tl[i] = 0.5 * (Sl + Tg) 205 | 206 | self.threshold[labels == i] = Tl[i] 207 | 208 | if self.gaussian is None or self.gaussian.shape[0] != Configuration.sp_kernel_size: 209 | self.gaussian = cv2.getGaussianKernel(Configuration.sp_kernel_size, -1, cv2.CV_32F) 210 | 211 | self.threshold = cv2.sepFilter2D(self.threshold, cv2.CV_32F, self.gaussian, self.gaussian) 212 | 213 | return np.uint8((self.threshold - rb) <= 0)# * mask 214 | 215 | class CDSunRemoval: 216 | @staticmethod 217 | def circle_mask(pos, radius, shape): 218 | cy, cx = pos 219 | x, y = np.ogrid[:shape[0], :shape[1]] 220 | return (x - cx)*(x - cx) + (y - cy)*(y - cy) < radius*radius 221 | 222 | @staticmethod 223 | def find_sun(start_pos, mask, static_mask=None): 224 | x = int(start_pos[0]) 225 | y = int(start_pos[1]) 226 | 227 | pos = start_pos 228 | radius = 1 229 | 230 | # fully white images 231 | if np.sum(mask == 0) < 100: 232 | return None, None, None 233 | 234 | # exception thrown when index out of bounds 235 | if x < 0 or y < 0 or x >= mask.shape[1] or y >= mask.shape[0]: 236 | #print('Coordinates not in image') 237 | #return None, None, None 238 | circle = 0 239 | while(np.sum(circle) == 0): 240 | circle = CDSunRemoval.circle_mask(pos, radius, mask.shape) 241 | radius += 2 242 | else: 243 | # Sun not found 244 | if mask[y, x] == 0: 245 | #print('Coordinates not saturated') 246 | return None, None, None 247 | 248 | # fix mask at border and get rid of tiny holes 249 | mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.array(CDSunRemoval.circle_mask((5, 5), 5.1, (11, 11)), np.uint8)) 250 | 251 | # index grid used for centering 252 | index_grid_x, index_grid_y = np.mgrid[:mask.shape[0], :mask.shape[1]] 253 | 254 | # update function when parameters change 255 | def _update_find_sun(pos, radius, mask): 256 | circle = CDSunRemoval.circle_mask(pos, radius, mask.shape) 257 | weights = np.logical_and(mask, circle) 258 | score = np.sum(weights) / np.sum(circle) 259 | 260 | return circle, weights, score 261 | 262 | def _get_outside(pos, radius, shape): 263 | cy, cx = pos 264 | x, y = np.mgrid[np.floor(cx - radius):np.ceil(cx + radius), np.floor(cy - radius):np.ceil(cy + radius)] 265 | res = np.logical_and((x - cx)*(x - cx) + (y - cy)*(y - cy) < radius*radius, np.logical_or(np.logical_or(x < 0, x >= shape[0]), np.logical_or(y < 0, y >= shape[1]))) 266 | return x[res], y[res] 267 | 268 | # first phase: radius doubling 269 | score = 1 270 | while score == 1: 271 | radius *= 2 272 | circle, weights, score = _update_find_sun(pos, radius, mask) 273 | 274 | # second phase: radius binary search 275 | hi = radius 276 | lo = radius / 2 277 | 278 | while hi - lo > 1: 279 | radius = np.round((hi + lo) / 2) 280 | circle, weights, score = _update_find_sun(pos, radius, mask) 281 | if score == 1: 282 | lo = radius 283 | else: 284 | hi = radius 285 | 286 | radius = lo 287 | 288 | # if the score is 0 now in the static mask, we give up 289 | if static_mask is None: 290 | static_mask = CloudDetectionHelper().mask 291 | 292 | static_score = np.sum(np.logical_and(mask + static_mask - 1, circle)) 293 | 294 | if static_score == 0: 295 | return None, None, None 296 | 297 | old_old_radius = radius - 1 298 | circle_sum_max = 0 299 | circle_sum_params = (pos, radius) 300 | circle_sum_iter = 0 301 | circle_sum_max_iter = 5 302 | 303 | while old_old_radius < radius and circle_sum_iter < circle_sum_max_iter: 304 | old_old_radius = radius 305 | old_params = (pos, radius) 306 | 307 | # third phase: centering 308 | 309 | old_score = score - 0.1 310 | while old_score < score: 311 | old_score = score 312 | old_pos = pos 313 | # getting outside for circles that are not fully inside the picture 314 | ox, oy = _get_outside(pos, radius, mask.shape) 315 | pos = (np.mean(np.concatenate((oy, index_grid_y[weights]))), np.mean(np.concatenate((index_grid_x[weights], ox)))) 316 | circle, weights, score = _update_find_sun(pos, radius, mask) 317 | 318 | pos = old_pos 319 | 320 | # fourth phase: radius increment 321 | 322 | old_radius = radius 323 | while score > 0.99: 324 | old_radius = radius 325 | radius += 1 326 | circle, weights, score = _update_find_sun(pos, radius, mask) 327 | 328 | radius = old_radius 329 | 330 | circle_sum_iter += 1 331 | circle_sum = np.sum(circle) 332 | if circle_sum > circle_sum_max: 333 | circle_sum_max = circle_sum 334 | circle_sum_params = (pos, radius) 335 | circle_sum_iter = 0 336 | 337 | if circle_sum_iter >= circle_sum_max_iter: 338 | pos, radius = circle_sum_params 339 | else: 340 | pos, radius = old_params 341 | 342 | if not np.isscalar(pos[0]): 343 | pos = (pos[0][0], pos[1][0]) 344 | 345 | return CDSunRemoval.circle_mask(pos, radius, mask.shape), pos, radius 346 | 347 | @staticmethod 348 | def find_sun_line(image, pos): 349 | # we start off with converting the image to a gray brightness image 350 | 351 | res = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 352 | 353 | # parameters: 354 | 355 | search_area = 20 356 | invalid_area = 4 357 | canny_threshold = 25 358 | ratio = 3 # 2-3 359 | hough_threshold = 10 360 | 361 | # first determine the center line, if there is one, by finding the maximum median (or if saturated the mean) 362 | 363 | max_mean = 0 364 | max_med = 0 365 | max_med_pos = pos[0] 366 | 367 | min_med = 255 368 | 369 | for x in range(np.max([0, int(pos[0]) - search_area]), np.min([image.shape[1], int(pos[0]) + search_area + 1])): 370 | mean = np.mean(res[:, x]) 371 | med = np.median(res[:, x]) 372 | if med == max_med or med > 250: 373 | max_med = med 374 | if mean > max_mean: 375 | max_mean = mean 376 | max_med_pos = x 377 | elif med > max_med: 378 | max_med = med 379 | max_med_pos = x 380 | max_brightness = mean 381 | if med < min_med: 382 | min_med = med 383 | 384 | # check if there is a line expecting a minimum value for the maximal median 385 | # and the position of it close enough to the sun 386 | 387 | if max_med < 200 or np.abs(max_med_pos - pos[0]) > search_area - invalid_area or min_med + 25 > max_med: 388 | return None, None, None 389 | 390 | # now we detect all edge lines via canny edge detection and hough transform for lines 391 | 392 | res = cv2.blur(res, (3, 3)) 393 | edges = cv2.Canny(res, canny_threshold, canny_threshold * ratio) 394 | 395 | lines = cv2.HoughLinesP(edges, 1, np.pi / 180, hough_threshold, minLineLength=20) 396 | 397 | # to determine the width of our broad line we now check the detected edge lines close to the center line 398 | 399 | min_x = max_med_pos 400 | max_x = max_med_pos 401 | 402 | for line in lines: 403 | line = line[0] 404 | 405 | # we use only vertical lines 406 | 407 | if line[0] - line[2] == 0: 408 | x = line[0] 409 | 410 | # filter out the antenna at 284-298 411 | 412 | if x >= 284 and x <= 298: 413 | if line[1] < 160 and line[3] < 160 and np.abs(x - max_med_pos) > invalid_area: 414 | continue 415 | 416 | # filter out the center line artifact at 318 and 321 417 | 418 | if x == 318 or x == 321: 419 | if np.abs(x - max_med_pos) > invalid_area: 420 | continue 421 | 422 | # take close lines to broaden 423 | 424 | if x < min_x and x > max_med_pos - search_area: 425 | min_x = x 426 | elif x > max_x and x < max_med_pos + search_area: 427 | max_x = x 428 | 429 | # return the line mask 430 | 431 | result = np.zeros(res.shape, np.uint8) 432 | result[:, min_x:max_x + 1] = True 433 | return result, min_x, max_x 434 | 435 | class CloudDetectionHelper: 436 | def __init__(self): 437 | self.mask = skycamera.SkyCamera.getBitMask() 438 | 439 | self.kernel = None 440 | 441 | def get_mask(self, image): 442 | b,g,r = cv2.split(image) 443 | 444 | if self.kernel is None or self.kernel.shape[0] != Configuration.morphology_kernel_size: 445 | self.kernel = np.ones((Configuration.morphology_kernel_size, Configuration.morphology_kernel_size), np.uint8) 446 | 447 | saturated = np.uint8(cv2.bitwise_and(cv2.bitwise_and(b, g), r) > 254) 448 | saturated = cv2.dilate(saturated, self.kernel, iterations = Configuration.morphology_iterations) 449 | 450 | self.fullmask = np.uint8(np.logical_or(np.logical_not(self.mask), saturated)) 451 | 452 | return self.fullmask 453 | 454 | def close_result(self, result): 455 | return cv2.morphologyEx(result, cv2.MORPH_CLOSE, self.kernel) 456 | 457 | def get_result_image(self, result): 458 | result_image = result * 255 459 | 460 | result_image[self.fullmask != 0] = 128 461 | 462 | return result_image 463 | 464 | def get_cloudiness(self, result): 465 | usable_part = result[self.fullmask == 0] 466 | 467 | return np.sum(usable_part) / usable_part.size 468 | 469 | def get_unsaturated(self): 470 | return np.sum(self.fullmask == 0) / np.sum(self.mask != 0) 471 | 472 | 473 | class CDSST: 474 | def __init__(self): 475 | path = 'frames/' 476 | day_images = SkyCameraFile.glob(path) 477 | day_images.sort() 478 | times = np.array([SkyCameraFile.parseTime(x).datetime for x in day_images]) 479 | 480 | self.day_images = day_images 481 | self.times = times 482 | 483 | self.calibration = Calibration(catalog=SkyCatalog(True)) 484 | self.calibration.load() 485 | 486 | self.helper = CloudDetectionHelper() 487 | 488 | try: 489 | with open('cd_sst.cache', 'rb') as f: 490 | self.cache = pickle.load(f) 491 | except: 492 | self.cache = pd.DataFrame(index=pd.Index([], dtype=np.datetime64), columns=['pos_x', 'pos_y', 'radius', 'stripe_min', 'stripe_max']) 493 | 494 | def save_cache(self): 495 | with open('cd_sst.cache', 'wb') as f: 496 | pickle.dump(self.cache, f) 497 | 498 | def detect(self, filename): 499 | image = cv2.imread(filename) 500 | time = SkyCameraFile.parseTime(filename).datetime 501 | 502 | mask = np.uint8(self.helper.get_mask(image).copy()) 503 | 504 | self.calibration.selectImage(filename) 505 | pos = self.calibration.project() 506 | pos = (pos[0], pos[1]) 507 | 508 | if time in self.cache.index: 509 | sun_x, sun_y, radius, min_x, max_x = self.cache.loc[time] 510 | 511 | sun = None 512 | sun_pos = None 513 | 514 | if not np.isnan(sun_x): 515 | sun_pos = (sun_x, sun_y) 516 | sun = CDSunRemoval.circle_mask(sun_pos, radius, mask.shape) 517 | 518 | sun_line = None 519 | if not np.isnan(min_x): 520 | sun_line = np.zeros(mask.shape, np.uint8) 521 | sun_line[:, min_x:max_x + 1] = True 522 | 523 | else: 524 | sun_line, min_x, max_x = CDSunRemoval.find_sun_line(image, pos) 525 | 526 | sun, sun_pos, radius = CDSunRemoval.find_sun(pos, mask) 527 | 528 | sun_x = sun_y = None 529 | if sun_pos is not None: 530 | sun_x = sun_pos[0] 531 | sun_y = sun_pos[1] 532 | 533 | self.cache.loc[time] = [sun_x, sun_y, radius, min_x, max_x] 534 | 535 | if sun_pos is None: 536 | sun_pos = pos 537 | 538 | mask = self.helper.fullmask.copy() 539 | mask[self.helper.mask == 0] = 0 540 | mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((11, 11), np.uint8)) 541 | 542 | _, contours, _ = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 543 | 544 | cloudiness = np.ones(mask.shape, np.float32) * .5 545 | 546 | did_sun = False 547 | 548 | for contour in contours: 549 | area = cv2.contourArea(contour) 550 | is_sun = False 551 | 552 | if area > 100: # TODO: try different numbers 553 | single_contour = np.zeros(mask.shape, np.uint8) 554 | cv2.drawContours(single_contour, [contour], 0, 1, cv2.FILLED) 555 | 556 | if sun is not None and not did_sun: 557 | sun_area = np.sum(sun[self.helper.mask == 1]) 558 | 559 | if area > 0.9 * sun_area: 560 | joint_area = np.sum(np.logical_and(single_contour, sun)) 561 | 562 | if sun_area / joint_area > 0.9: 563 | is_sun = True 564 | 565 | if is_sun: 566 | if sun_area * 1.2 < area: 567 | difference = np.uint8(np.logical_and(np.logical_not(sun), single_contour)) 568 | _, contours2, _ = cv2.findContours(difference, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 569 | # filter smaller ones here! currently done with the if at the beginning 570 | contours += contours2 571 | did_sun = True 572 | 573 | if not is_sun: 574 | cloudiness[single_contour > 0] = 1.0 575 | 576 | 577 | 578 | b, g, r = cv2.split(np.int32(image)) 579 | 580 | mask = self.helper.mask 581 | 582 | rmb = r - b 583 | rmb[mask == 0] = 0 584 | 585 | cloudiness[rmb < -10] = 0 586 | cloudiness[rmb > 50] = 1 587 | 588 | delta = np.timedelta64(39, 's') 589 | delta2 = np.timedelta64(0, 's') 590 | time_diff = time - self.times 591 | before = np.logical_and(time_diff > delta2, time_diff < delta) 592 | 593 | if np.sum(before) == 0: 594 | raise ValueError('No previous image found.') 595 | 596 | current_index = np.where(before)[0][0] 597 | 598 | prev_img = cv2.imread(self.day_images[current_index]) 599 | 600 | gray_prev = cv2.cvtColor(prev_img, cv2.COLOR_BGR2GRAY) 601 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 602 | 603 | flow = cv2.calcOpticalFlowFarneback(gray_prev, gray_image, None, 0.5, 3, 15, 3, 5, 1.2, 0) 604 | flow2 = -cv2.calcOpticalFlowFarneback(gray_image, gray_prev, None, 0.5, 3, 15, 3, 5, 1.2, 0) 605 | 606 | mag1, _ = cv2.cartToPolar(flow[...,0], flow[...,1]) 607 | mag2, _ = cv2.cartToPolar(flow2[...,0], flow2[...,1]) 608 | 609 | movement = np.logical_and(mag1 > 1, mag2 > 1) 610 | no_movement = np.logical_not(movement) 611 | 612 | brightness = np.mean(image, 2) 613 | 614 | cloudiness[np.logical_and(movement == 1, cloudiness != 0)] = 1 615 | cloudiness[self.helper.mask == 0] = 1 616 | if sun is not None: 617 | cloudiness[sun == 1] = 1 618 | 619 | if sun_line is not None: 620 | sun_line_dilated = cv2.morphologyEx(sun_line, cv2.MORPH_DILATE, np.ones((1, 3))) 621 | cloudiness[sun_line_dilated == 1] = 1 622 | 623 | y, x = np.mgrid[0:brightness.shape[0], 0:brightness.shape[1]] 624 | 625 | x1 = [] 626 | y1 = [] 627 | out = [] 628 | 629 | for i in range(brightness.shape[0]): 630 | for j in range(brightness.shape[1]): 631 | if cloudiness[i, j] != 1: 632 | x1.append(x[i, j]) 633 | y1.append(y[i, j]) 634 | out.append(brightness[i, j]) 635 | 636 | x = np.array(x1) - sun_pos[0] 637 | y = np.array(y1) - sun_pos[1] 638 | 639 | out = np.array(out) 640 | 641 | dist = np.sqrt(x * x + y * y) 642 | 643 | A = np.array([dist, np.ones(x.shape), x, y]).transpose() 644 | A_inv = np.linalg.pinv(A) 645 | 646 | param = np.dot(A_inv, out) 647 | 648 | y, x = np.mgrid[0:brightness.shape[0], 0:brightness.shape[1]] 649 | x = x - pos[0] 650 | y = y - pos[1] 651 | dist = np.sqrt(x * x + y * y) 652 | A = np.array([dist, np.ones(x.shape), x, y]).transpose() 653 | gradient = np.dot(A, param).transpose() 654 | 655 | rect_size = 15 656 | 657 | rect_border = (rect_size - 1) // 2 658 | 659 | brightness_norm = brightness - gradient 660 | 661 | stddev = np.zeros(brightness.shape) 662 | 663 | for y in range(image.shape[0]): 664 | for x in range(image.shape[1]): 665 | if cloudiness[y, x] == 1: 666 | continue 667 | 668 | lx = x - rect_border 669 | rx = x + rect_border + 1 670 | uy = y - rect_border 671 | dy = y + rect_border + 1 672 | 673 | if lx < 0: lx = 0 674 | if uy < 0: uy = 0 675 | if rx > image.shape[1]: rx = image.shape[1] 676 | if dy > image.shape[0]: dy = image.shape[0] 677 | 678 | mask_part = cloudiness[uy:dy, lx:rx] 679 | stddev[y, x] = np.std(brightness_norm[uy:dy, lx:rx][mask_part != 1]) 680 | 681 | def_clear = np.sum(cloudiness == 0) 682 | 683 | cloudiness[cloudiness == 0.5] = (stddev > 3)[cloudiness == 0.5] 684 | 685 | if sun is None or (sun_line is None and radius < 100): 686 | if def_clear < 0.1 * np.sum(self.helper.mask == 1): 687 | cloudiness[np.logical_and(cloudiness == 0, rmb > -8)] = 1 688 | 689 | cloudiness = self.helper.close_result(cloudiness) 690 | 691 | cloudiness[self.helper.mask == 0] = 0.5 692 | 693 | if sun is not None: 694 | cloudiness[sun == 1] = 0.5 695 | 696 | if sun_line is not None: 697 | cloudiness[sun_line_dilated == 1] = 0.5 698 | 699 | return cloudiness 700 | 701 | def get_cloud_cover(self, cloudiness): 702 | return np.sum(cloudiness == 1) / np.sum(cloudiness != 0.5) 703 | 704 | if __name__ == '__main__': 705 | import sys 706 | 707 | if len(sys.argv) < 2: 708 | print('Usage: cloude_detection ') 709 | sys.exit(1) 710 | 711 | filename = sys.argv[1] 712 | 713 | image = cv2.imread(filename, 1) 714 | 715 | #detector = CDRBDifference() 716 | #detector = CDRBRatio() 717 | #detector = CDBRRatio() 718 | #detector = CDAdaptive() 719 | #detector = CDBackground() 720 | #detector = CDMulticolor() 721 | detector = CDSuperPixel() 722 | helper = CloudDetectionHelper() 723 | 724 | result = helper.close_result(detector.detect(image, helper.get_mask(image))) 725 | 726 | #cv2.imwrite("result.png", helper.get_result_image(result)) 727 | 728 | print(helper.get_cloudiness(result), helper.get_unsaturated()) 729 | -------------------------------------------------------------------------------- /configuration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | from astropy import units as u 22 | import sys 23 | 24 | class Configuration: 25 | ## General Settings 26 | 27 | # Observer Position 28 | latitude = 47.06713 * u.deg 29 | longitude = 15.49343 * u.deg 30 | elevation = 493 * u.m 31 | 32 | # Angles for sun and moon determination 33 | night_angle = -6 * u.deg 34 | day_angle = 6 * u.deg 35 | moon_up_angle = 0 * u.deg 36 | 37 | # Logging 38 | logging = False 39 | log_file = 'log.txt' 40 | 41 | ## Recording and Camera Settings 42 | 43 | # Recording Settings 44 | day_averaging_frames = 100 45 | night_averaging_frames = 1 46 | default_storage_path = 'frames' 47 | day_time_between_frames = 20 # seconds 48 | night_time_between_frames = 0#1#0 # seconds 49 | frame_count = -1 # <= 0 means infinite 50 | show_recorded_frames = True 51 | store_in_subdirectory = True 52 | 53 | # Settings for dynamic night frame averaging 54 | dnfa_enabled = True 55 | dnfa_window_size = 150 56 | dnfa_min_med_diff_factor = 0.2 57 | dnfa_min_diff_value = 0.3 58 | dnfa_min_frames = 50 59 | dnfa_max_frames = 200 60 | 61 | # Camera Settings 62 | control_settings = True 63 | 64 | serial_port = 7 65 | time_between_commands = 0.2 # seconds 66 | verbose_commands = True 67 | 68 | # these settings should be set and are the same for day and night: "BLC0", "FLC0", "PRAG", "CMDA", "GATB", "ENMI" 69 | day_settings = ["SSH0", "SSX0", "SAES", "AGC0"] 70 | night_settings = ["SSH1", "SSX7", "SALC", "ALC0", "AGC1"] 71 | # shooting star settings? 72 | #night_settings = ["SSH1", "SSX4", "SALC", "ALC0", "AGC1"] 73 | 74 | ## Day time Algorithm Settings 75 | rb_difference_threshold = 6 76 | rb_ratio_threshold = 0.9 # 0.6 77 | br_ratio_threshold = 1 # 1.3 78 | nbr_threshold = 0.25 79 | adaptive_threshold = -10 80 | adaptive_block_size = 127 81 | background_rect_size = 99 82 | mc_rb_threshold = 20 83 | mc_bg_threshold = 20 84 | mc_b_threshold = 250 85 | sp_num_superpixels = 100 86 | sp_prior = 2 87 | sp_num_levels = 4 88 | sp_num_histogram_bins = 5 89 | sp_num_iterations = 4 90 | sp_kernel_size = 5 91 | morphology_kernel_size = 3 92 | morphology_iterations = 2 93 | 94 | ## Night time Algorithm Settings 95 | 96 | # Configuration Files 97 | calibration_file = "calibration.dat" 98 | correspondence_file = "correspondences.dat" 99 | configuration_file = "configuration.dat" 100 | star_catalog_file = "catalog" 101 | mask_file = "mask.png" 102 | 103 | # Algorithm Settings 104 | min_alt = 10 105 | alt_step = 20 106 | az_step = 60 107 | max_mag = 2.5 108 | 109 | # Star Finder 110 | gaussian_roi_size = 5 111 | gaussian_threshold = 0.2 112 | gaussian_kernel_size = 33 113 | 114 | candidate_radius = 5 115 | 116 | gftt_max_corners = 600 117 | gftt_quality_level = 0.002 118 | gftt_min_distance = 5 119 | 120 | surf_threshold = 1 121 | 122 | log_max_rect_size = 10 123 | log_block_size = 9 124 | log_threshold = 0.53 125 | log_kernel_size = 5 126 | 127 | ## Difference Settings 128 | 129 | # Difference View Settings 130 | difference_detection_window_size = 10 131 | 132 | # here is the configuration for my development machine which runs linux, not windows 133 | if sys.platform == 'linux': 134 | # Camera Settings 135 | Configuration.serial_port = '/dev/ttyS4' 136 | -------------------------------------------------------------------------------- /control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import serial 22 | import binascii 23 | import time 24 | from configuration import Configuration 25 | 26 | class SkyCameraControl: 27 | commands = { 28 | "ENMI": ["808801010000F6", "enhancer: middle"], 29 | "ENHI": ["808800010000F7", "enhancer: high"], 30 | "GATA": ["808900010000F6", "gamma: type A"], 31 | "GATB": ["808901010000F5", "gamma: type B"], 32 | "CMAU": ["808E00010000F1", "color mode: auto"], 33 | "CMDA": ["808E01010000F0", "color mode: day"], 34 | "CMNI": ["808E02010000EF", "color mode: night"], 35 | "CMEX": ["808E03010000EE", "color mode: ext"], 36 | "WAWB": ["808301010000FB", "white balance: AWB"], 37 | "WATW": ["808300010000FC", "white balance: ATW"], 38 | "PRSS": ["818B01010000F2", "priority: slow shutter"], 39 | "PRAG": ["818B00010000F3", "priority: AGC"], 40 | "FLC1": ["808C01010000F2", "FLC: on"], 41 | "FLC0": ["808C00010000F3", "FLC: off"], 42 | "AGC1": ["808201010000FC", "AGC: on"], 43 | "AGC0": ["808200010000FD", "AGC: off"], 44 | "BLC1": ["808601010000F8", "BLC: on"], 45 | "BLC0": ["808600010000F9", "BLC: off"], 46 | "SAES": ["808B00010000F4", "AES"], 47 | "SALC": ["808B01010000F3", "ALC"], 48 | "ALC7": ["808F07010000E9", "ALC: 1/10000"], 49 | "ALC6": ["808F06010000EA", "ALC: 1/4000"], 50 | "ALC5": ["808F05010000EB", "ALC: 1/2000"], 51 | "ALC4": ["808F04010000EC", "ALC: 1/1000"], 52 | "ALC3": ["808F03010000ED", "ALC: 1/500"], 53 | "ALC2": ["808F02010000EE", "ALC: 1/250"], 54 | "ALC1": ["808F01010000EF", "ALC: 1/100"], 55 | "ALC0": ["808F00010000F0", "ALC: OFF"], 56 | "SSH1": ["808D01010000F1", "slow shutter: on"], 57 | "SSH0": ["808D00010000F2", "slow shutter: off"], 58 | "SSX0": ["818200010000FC", "slow shutter: x2"], 59 | "SSX1": ["818201010000FB", "slow shutter: x4"], 60 | "SSX2": ["818202010000FA", "slow shutter: x8"], 61 | "SSX3": ["818203010000F9", "slow shutter: x16"], 62 | "SSX4": ["818204010000F8", "slow shutter: x32"], 63 | "SSX5": ["818205010000F7", "slow shutter: x64"], 64 | "SSX6": ["818206010000F6", "slow shutter: x128"], 65 | "SSX7": ["818207010000F5", "slow shutter: x256"], 66 | } 67 | 68 | def __init__(self, port = Configuration.serial_port): 69 | self.port = port 70 | self.ser = None 71 | 72 | def open(self): 73 | self.ser = serial.Serial(self.port) 74 | 75 | def close(self): 76 | self.ser.close() 77 | self.ser = None 78 | 79 | def sendCommand(self, command, verbose = False): 80 | if self.ser is None: 81 | return 82 | 83 | if verbose: 84 | print("Sending command \"{0}\": {1}".format(SkyCameraControl.commands[command][1], SkyCameraControl.commands[command][0])) 85 | self.ser.write(bytearray(binascii.unhexlify(SkyCameraControl.commands[command][0]))) 86 | 87 | def switchConfiguration(self, night, verbose = False): 88 | if night: 89 | commands = Configuration.night_settings 90 | else: 91 | commands = Configuration.day_settings 92 | 93 | self.open() 94 | 95 | for command in commands: 96 | self.sendCommand(command, verbose) 97 | time.sleep(Configuration.time_between_commands) 98 | 99 | self.close() 100 | 101 | if __name__ == '__main__': 102 | import sys 103 | 104 | if len(sys.argv) < 2: 105 | print('Usage: control 0|1|...') 106 | exit(1) 107 | 108 | commands = [] 109 | 110 | if sys.argv[1] == '0': 111 | commands = Configuration.day_settings 112 | elif sys.argv[1] == '1': 113 | commands = Configuration.night_settings 114 | else: 115 | commands = sys.argv[1:] 116 | 117 | control = SkyCameraControl(Configuration.serial_port) 118 | control.open() 119 | 120 | for command in commands: 121 | control.sendCommand(command, Configuration.verbose_commands) 122 | time.sleep(Configuration.time_between_commands) 123 | 124 | control.close() 125 | -------------------------------------------------------------------------------- /frame_difference.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import cv2 22 | import sys 23 | import numpy as np 24 | import os.path 25 | import glob 26 | from skycamera import SkyCamera 27 | 28 | class FrameDifference: 29 | def __init__(self): 30 | self.mask = cv2.cvtColor(SkyCamera.getBitMask(), cv2.COLOR_GRAY2BGR) 31 | 32 | def difference(self, image1, image2): 33 | image1 = np.int32(image1 * self.mask) 34 | image2 = np.int32(image2 * self.mask) 35 | 36 | self.diff = image1 - image2 37 | 38 | def getValue(self): 39 | return np.mean(np.abs(self.diff)) 40 | 41 | def getImage(self): 42 | return cv2.cvtColor(np.uint8(np.abs(self.diff)), cv2.COLOR_BGR2GRAY) 43 | 44 | class FrameComparison: 45 | def __init__(self, files): 46 | if len(files) < 2: 47 | raise Exception('Need at least two files to compare.') 48 | 49 | self.image_window = 'Image' 50 | self.threshold_window = 'Threshold' 51 | self.difference_window = 'Difference' 52 | self.files = files 53 | self.tb_threshold = 'Threshold' 54 | self.tb_image = 'Image' 55 | self.current_image = 0 56 | 57 | self.image1 = None 58 | self.image2 = None 59 | self.difference = None 60 | self.threshold = 25 61 | self.gray = None 62 | 63 | cv2.namedWindow(self.image_window, cv2.WINDOW_AUTOSIZE) 64 | cv2.namedWindow(self.difference_window, cv2.WINDOW_AUTOSIZE) 65 | cv2.namedWindow(self.threshold_window, cv2.WINDOW_AUTOSIZE) 66 | cv2.createTrackbar(self.tb_image, self.difference_window, 0, len(self.files) - 2, self.selectImage) 67 | cv2.createTrackbar(self.tb_threshold, self.threshold_window, self.threshold, 255, self.renderThreshold) 68 | self.render() 69 | 70 | def selectImage(self, number): 71 | if number >= len(self.files) - 1 or number < 0: 72 | return 73 | 74 | self.current_image = number 75 | self.render() 76 | 77 | def render(self): 78 | self.image1 = np.int32(cv2.imread(self.files[self.current_image], 1)) 79 | self.image2 = np.int32(cv2.imread(self.files[self.current_image + 1], 1)) 80 | 81 | self.difference = self.image1 - self.image2 82 | 83 | self.gray = cv2.cvtColor(np.uint8(np.abs(self.difference)), cv2.COLOR_BGR2GRAY) 84 | 85 | self.difference = np.uint8(self.difference * 2 + 128) 86 | 87 | cv2.imshow(self.image_window, np.uint8(self.image1)) 88 | #cv2.imshow(self.difference_window, self.difference) 89 | cv2.imshow(self.difference_window, self.gray) 90 | self.renderThreshold(self.threshold) 91 | 92 | def renderThreshold(self, threshold): 93 | self.threshold = threshold 94 | _, thresh = cv2.threshold(self.gray, threshold, 255, cv2.THRESH_BINARY) 95 | cv2.imshow(self.threshold_window, thresh) 96 | 97 | def run(self): 98 | while(True): 99 | k = cv2.waitKey(30) & 0xFF 100 | if k == 27: 101 | break 102 | if k == ord('s'): 103 | filename = 'out.png' 104 | print('Saving ' + filename) 105 | cv2.imwrite(filename, self.difference) 106 | 107 | cv2.destroyAllWindows() 108 | 109 | 110 | if __name__ == '__main__': 111 | if len(sys.argv) < 2: 112 | print('Usage: frame_difference ...') 113 | exit(1) 114 | 115 | files = [] 116 | 117 | for path in sys.argv[1:]: 118 | if os.path.isfile(path): 119 | files.append(path) 120 | elif os.path.isdir(path): 121 | files += sorted(glob.glob(os.path.join(path, "*.jpg"))) 122 | else: 123 | print('Invalid parameter: %s' % path) 124 | 125 | frame_comparison = FrameComparison(files) 126 | 127 | frame_comparison.run() 128 | 129 | 130 | 131 | 132 | 133 | 134 | #__import__("code").interact(local=locals()) 135 | 136 | -------------------------------------------------------------------------------- /image_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ImageWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1178 10 | 694 11 | 12 | 13 | 14 | Image Viewer 15 | 16 | 17 | 18 | 19 | 20 | Qt::Horizontal 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 0 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 0 44 | 45 | 46 | 47 | 48 | 49 | 50 | Qt::AlignCenter 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Qt::Horizontal 61 | 62 | 63 | 64 | 65 | 66 | 67 | << 68 | 69 | 70 | 71 | 72 | 73 | 74 | < 75 | 76 | 77 | 78 | 79 | 80 | 81 | > 82 | 83 | 84 | 85 | 86 | 87 | 88 | >> 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 0 101 | 0 102 | 103 | 104 | 105 | 106 | 107 | 108 | Display 109 | 110 | 111 | 112 | 113 | 114 | Im&age 115 | 116 | 117 | true 118 | 119 | 120 | 121 | 122 | 123 | 124 | C&louds 125 | 126 | 127 | false 128 | 129 | 130 | 131 | 132 | 133 | 134 | Stars 135 | 136 | 137 | 138 | 139 | 140 | 141 | Differen&ce 142 | 143 | 144 | 145 | 146 | 147 | 148 | &Auto-Refresh 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Cloud Algorithm 159 | 160 | 161 | 162 | 163 | 164 | 165 | R-B Difference 166 | 167 | 168 | 169 | 170 | R/B Ratio 171 | 172 | 173 | 174 | 175 | B/R Ratio 176 | 177 | 178 | 179 | 180 | Normalized B/R Ratio 181 | 182 | 183 | 184 | 185 | R-B Adaptive Threshold 186 | 187 | 188 | 189 | 190 | Multicolor Criterion 191 | 192 | 193 | 194 | 195 | Background Subtraction 196 | 197 | 198 | 199 | 200 | Super Pixel Segmentation 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | Stars 212 | 213 | 214 | 215 | 216 | 217 | 218 | Gaussian 219 | 220 | 221 | 222 | 223 | FAST 224 | 225 | 226 | 227 | 228 | GFTT 229 | 230 | 231 | 232 | 233 | SURF 234 | 235 | 236 | 237 | 238 | LoG 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | Difference Viewing 250 | 251 | 252 | 253 | 254 | 255 | << 256 | 257 | 258 | 259 | 260 | 261 | 262 | < 263 | 264 | 265 | 266 | 267 | 268 | 269 | > 270 | 271 | 272 | 273 | 274 | 275 | 276 | >> 277 | 278 | 279 | 280 | 281 | 282 | 283 | Qt::Horizontal 284 | 285 | 286 | 287 | 40 288 | 20 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | Qt::Vertical 300 | 301 | 302 | 303 | 225 304 | 0 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import sys 22 | import cv2 23 | import os.path 24 | import pickle 25 | from PyQt5 import QtCore, QtGui, QtWidgets 26 | from main_ui import Ui_MainWindow 27 | from image_view_ui import Ui_ImageWidget 28 | from settings_view_ui import Ui_SettingsWidget 29 | from configuration import Configuration 30 | from skycamerafile import SkyCameraFile 31 | from cloud_detection import * 32 | from frame_difference import FrameDifference 33 | from star_checker import StarCheckerHelper 34 | from star_detection import * 35 | from calibration import StarCorrespondence 36 | from configuration import Configuration 37 | 38 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 39 | from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 40 | import matplotlib.pyplot as plt 41 | 42 | 43 | class PlotWidget(QtWidgets.QWidget): 44 | def __init__(self, parent=None): 45 | QtWidgets.QWidget.__init__(self, parent) 46 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 47 | 48 | self.figure = plt.figure() 49 | 50 | self.canvas = FigureCanvas(self.figure) 51 | 52 | self.toolbar = NavigationToolbar(self.canvas, self) 53 | 54 | self.button = QtWidgets.QPushButton("Plot") 55 | self.button.clicked.connect(self.plot) 56 | 57 | layout = QtWidgets.QVBoxLayout() 58 | layout.addWidget(self.toolbar) 59 | layout.addWidget(self.canvas) 60 | layout.addWidget(self.button) 61 | 62 | self.setLayout(layout) 63 | 64 | def plot(self): 65 | data = [x for x in range(10)] 66 | 67 | ax = self.figure.add_subplot(111) 68 | 69 | ax.hold(False) 70 | 71 | ax.plot(data, "*-") 72 | 73 | self.canvas.draw() 74 | 75 | 76 | class MainQT5(QtWidgets.QMainWindow): 77 | def __init__(self, parent=None): 78 | QtWidgets.QWidget.__init__(self, parent) 79 | 80 | self.ui = Ui_MainWindow() 81 | self.ui.setupUi(self) 82 | 83 | self.ui.action_New_View.triggered.connect(self.new_view) 84 | self.ui.action_Tile.triggered.connect(self.tile) 85 | self.ui.action_Settings.triggered.connect(self.settings) 86 | 87 | self.new_view().showMaximized() 88 | 89 | #self.plot() 90 | 91 | self.loadSettings() 92 | 93 | def plot(self): 94 | widget = PlotWidget() 95 | self.ui.mdiArea.addSubWindow(widget); 96 | widget.show() 97 | return widget 98 | 99 | def settings(self): 100 | widget = SettingsWidget() 101 | self.ui.mdiArea.addSubWindow(widget); 102 | widget.show() 103 | return widget 104 | 105 | def new_view(self): 106 | widget = ImageWidget() 107 | self.ui.mdiArea.addSubWindow(widget); 108 | widget.show() 109 | return widget 110 | 111 | def tile(self): 112 | if self.ui.mdiArea.currentSubWindow().isMaximized(): 113 | self.ui.mdiArea.currentSubWindow().showNormal() 114 | 115 | position = QtCore.QPoint(0, 0) 116 | 117 | for window in self.ui.mdiArea.subWindowList(): 118 | rect = QtCore.QRect(0, 0, self.ui.mdiArea.width(), self.ui.mdiArea.height() / len(self.ui.mdiArea.subWindowList())) 119 | window.setGeometry(rect) 120 | window.move(position) 121 | position.setY(position.y() + window.height()) 122 | 123 | def loadSettings(self): 124 | if not os.path.exists(Configuration.configuration_file): 125 | return 126 | 127 | dictionary = None 128 | 129 | with open(Configuration.configuration_file, 'rb') as f: 130 | dictionary = pickle.load(f) 131 | 132 | for name, value in dictionary.items(): 133 | setattr(Configuration, name, value) 134 | 135 | 136 | class ImageViewMode(): 137 | def __init__(self, view): 138 | self.view = view 139 | 140 | def getImage(self): 141 | return self.view.image 142 | 143 | class CloudViewMode(): 144 | def __init__(self, view): 145 | self.view = view 146 | self.detectors = [] 147 | self.detectors.append(CDRBDifference()) 148 | self.detectors.append(CDRBRatio()) 149 | self.detectors.append(CDBRRatio()) 150 | self.detectors.append(CDNBRRatio()) 151 | self.detectors.append(CDAdaptive()) 152 | self.detectors.append(CDMulticolor()) 153 | self.detectors.append(CDBackground()) 154 | self.detectors.append(CDSuperPixel()) 155 | self.helper = CloudDetectionHelper() 156 | self.current_detector = 0 157 | 158 | def setDetector(self, index): 159 | self.current_detector = index 160 | self.view.refresh() 161 | 162 | def getImage(self): 163 | image = self.view.image 164 | result = self.helper.close_result(self.detectors[self.current_detector].detect(image, self.helper.get_mask(image))) 165 | return self.helper.get_result_image(result) 166 | 167 | class StarViewMode(): 168 | def __init__(self, view): 169 | self.view = view 170 | self.detectors = [] 171 | self.detectors.append(GaussianStarFinder()) 172 | self.detectors.append(CandidateStarFinder(FASTStarDetector())) 173 | self.detectors.append(CandidateStarFinder(GFTTStarDetector())) 174 | self.detectors.append(CandidateStarFinder(SURFStarDetector())) 175 | self.detectors.append(CandidateStarFinder(LoGStarDetector())) 176 | self.helper = StarCheckerHelper(Configuration.calibration_file) 177 | self.current_detector = 0 178 | 179 | def setDetector(self, index): 180 | self.current_detector = index 181 | self.view.refresh() 182 | 183 | def getImage(self): 184 | self.helper.prepare(self.view.files[self.view.index], self.detectors[self.current_detector]) 185 | 186 | return self.helper.get_image() 187 | 188 | class DifferenceViewMode(): 189 | def __init__(self, view): 190 | self.difference = FrameDifference() 191 | self.view = view 192 | self.differences = np.array([]) 193 | self.window_size = Configuration.difference_detection_window_size 194 | 195 | def reset(self): 196 | self.differences = np.zeros(len(self.view.files)) 197 | self.differences[:] = np.nan 198 | 199 | def getImage(self): 200 | image1 = self.view.image 201 | image2 = image1 202 | if self.view.index > 0: 203 | image2 = cv2.imread(self.view.files[self.view.index - 1], 1) 204 | 205 | self.difference.difference(image1, image2) 206 | self.differences[self.view.index] = self.difference.getValue() 207 | return self.difference.getImage() 208 | 209 | def nextInteresting(self, backward = False): 210 | length = len(self.differences) 211 | 212 | if length < self.window_size: 213 | # err: not enough files 214 | if backward: 215 | self.view.selectFile(0) 216 | else: 217 | self.view.selectFile(len(self.differences) - 1) 218 | return 219 | 220 | step = 1 221 | if backward: 222 | step = -1 223 | 224 | index = self.view.index 225 | 226 | start = index - step * self.window_size 227 | if start < 0: 228 | start = 0 229 | if start >= length: 230 | start = length - 1 231 | 232 | end = length - 1 233 | if backward: 234 | end = 0 235 | 236 | for i in range(start, start + step * (self.window_size + 1), step): 237 | if np.isnan(self.differences[i]): 238 | self.view.selectFile(i) 239 | if self.view.modes[self.view.current_mode] != self.view.difference_mode: 240 | self.getImage() 241 | 242 | run = True 243 | 244 | index = start + step * self.window_size 245 | 246 | while run: 247 | index += step 248 | if index == end + step or index == end: 249 | index = end 250 | break 251 | 252 | start += step 253 | 254 | if np.isnan(self.differences[index]): 255 | self.view.selectFile(index) 256 | if self.view.modes[self.view.current_mode] != self.view.difference_mode: 257 | self.getImage() 258 | 259 | window = self.differences[start:start + step * self.window_size:step] 260 | mean = np.median(window) 261 | differences = np.abs(self.differences[start+step:start + step * (self.window_size + 1):step] - window) 262 | stdev = np.median(differences) 263 | 264 | if self.differences[index] > mean + stdev * 2: 265 | run = False 266 | 267 | self.view.selectFile(index) 268 | 269 | class ImageWidget(QtWidgets.QWidget): 270 | def __init__(self, parent=None): 271 | QtWidgets.QWidget.__init__(self, parent) 272 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 273 | 274 | self.modes = [] 275 | self.modes.append(ImageViewMode(self)) 276 | self.modes.append(CloudViewMode(self)) 277 | self.modes.append(StarViewMode(self)) 278 | self.difference_mode = DifferenceViewMode(self) 279 | self.modes.append(self.difference_mode) 280 | 281 | self.current_mode = 0 282 | self.index = 0 283 | self.files = [] 284 | 285 | self.ui = Ui_ImageWidget() 286 | self.ui.setupUi(self) 287 | 288 | self.filesystemmodel = QtWidgets.QFileSystemModel(self) 289 | self.filesystemmodel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs) 290 | self.filesystemmodel.setRootPath("/") 291 | self.ui.folderView.setModel(self.filesystemmodel) 292 | self.ui.folderView.hideColumn(1) 293 | self.ui.folderView.hideColumn(2) 294 | self.ui.folderView.hideColumn(3) 295 | self.ui.folderView.setCurrentIndex(self.filesystemmodel.index(Configuration.default_storage_path)) 296 | self.ui.folderView.clicked.connect(self.changePath) 297 | self.changePath(self.ui.folderView.currentIndex()) 298 | 299 | self.ui.imageSelector.valueChanged.connect(self.selectImage) 300 | self.ui.firstButton.clicked.connect(self.firstFile) 301 | self.ui.previousButton.clicked.connect(self.previousFile) 302 | self.ui.nextButton.clicked.connect(self.nextFile) 303 | self.ui.lastButton.clicked.connect(self.lastFile) 304 | 305 | self.ui.rbImage.clicked.connect(self.showImage) 306 | self.ui.rbClouds.clicked.connect(self.showClouds) 307 | self.ui.rbStars.clicked.connect(self.showStars) 308 | self.ui.rbDifference.clicked.connect(self.showDifference) 309 | 310 | self.ui.cbRefresh.toggled.connect(self.toggleAutoRefresh) 311 | 312 | self.ui.cbAlgorithm.currentIndexChanged.connect(self.modes[1].setDetector) 313 | self.ui.cbStars.currentIndexChanged.connect(self.modes[2].setDetector) 314 | 315 | self.ui.diffPrevious.clicked.connect(self.previousFile) 316 | self.ui.diffNext.clicked.connect(self.nextFile) 317 | self.ui.diffPreviousInteresting.clicked.connect(self.previousInteresting) 318 | self.ui.diffNextInteresting.clicked.connect(self.nextInteresting) 319 | 320 | self.timer = QtCore.QTimer(self) 321 | self.timer.setInterval(500) 322 | self.timer.timeout.connect(self.refresh) 323 | 324 | def toggleAutoRefresh(self, enable): 325 | if enable: 326 | self.timer.start() 327 | else: 328 | self.timer.stop() 329 | 330 | def previousInteresting(self): 331 | self.difference_mode.nextInteresting(True) 332 | 333 | def nextInteresting(self): 334 | self.difference_mode.nextInteresting(False) 335 | 336 | def activateMode(self, mode): 337 | if mode == self.current_mode: 338 | return 339 | 340 | self.current_mode = mode 341 | self.selectImage(self.index) 342 | 343 | def showImage(self): 344 | self.activateMode(0) 345 | 346 | def showClouds(self): 347 | self.activateMode(1) 348 | 349 | def showStars(self): 350 | self.activateMode(2) 351 | 352 | def showDifference(self): 353 | self.activateMode(3) 354 | 355 | def changePath(self, index): 356 | path = self.filesystemmodel.fileInfo(index).absoluteFilePath() 357 | self.files = SkyCameraFile.glob(path) 358 | if len(self.files) > 0: 359 | self.ui.imageSelector.setMaximum(len(self.files) - 1) 360 | self.ui.imageSelector.setEnabled(True) 361 | else: 362 | self.ui.imageSelector.setEnabled(False) 363 | 364 | self.difference_mode.reset() 365 | self.selectFile(0) 366 | 367 | def firstFile(self): 368 | self.selectFile(0) 369 | 370 | def previousFile(self): 371 | self.selectFile(self.ui.imageSelector.sliderPosition() - 1) 372 | 373 | def nextFile(self): 374 | self.selectFile(self.ui.imageSelector.sliderPosition() + 1) 375 | 376 | def lastFile(self): 377 | self.selectFile(len(self.files) - 1) 378 | 379 | def selectFile(self, index): 380 | self.ui.imageSelector.setSliderPosition(index) 381 | self.selectImage(self.ui.imageSelector.sliderPosition()) 382 | 383 | def refresh(self): 384 | self.selectImage(self.index) 385 | 386 | def selectImage(self, index): 387 | if index >= len(self.files) or index < 0: 388 | self.ui.imageView.setText("No images found.") 389 | return 390 | 391 | self.index = index 392 | self.image = cv2.imread(self.files[index], 1) 393 | 394 | image = self.modes[self.current_mode].getImage() 395 | 396 | if len(image.shape) < 3 or image.shape[2] == 1: 397 | image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) 398 | else: 399 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 400 | 401 | height, width, byteValue = self.image.shape 402 | byteValue = byteValue * width 403 | 404 | qimage = QtGui.QImage(image, width, height, byteValue, QtGui.QImage.Format_RGB888) 405 | 406 | self.ui.imageView.setPixmap(QtGui.QPixmap.fromImage(qimage)) 407 | 408 | class CSpinBox: 409 | def __init__(self, name, ui): 410 | self.name = name 411 | self.ui = getattr(ui, name) 412 | 413 | self.ui.setValue(getattr(Configuration, name)) 414 | self.ui.valueChanged.connect(self.updateValue) 415 | 416 | def updateValue(self, value): 417 | setattr(Configuration, self.name, value) 418 | 419 | class CCheckBox: 420 | def __init__(self, name, ui): 421 | self.name = name 422 | self.ui = getattr(ui, name) 423 | 424 | self.ui.setChecked(getattr(Configuration, name)) 425 | self.ui.toggled.connect(self.updateValue) 426 | 427 | def updateValue(self, value): 428 | setattr(Configuration, self.name, value) 429 | 430 | class CPathBox: 431 | def __init__(self, name, ui, button): 432 | self.name = name 433 | self.ui = getattr(ui, name) 434 | self.button = button 435 | 436 | self.ui.setText(getattr(Configuration, name)) 437 | self.ui.textChanged.connect(self.updatePath) 438 | button.clicked.connect(self.selectPath) 439 | 440 | def selectPath(self): 441 | directory = QtWidgets.QFileDialog.getExistingDirectory(None, "Choose directory", getattr(Configuration, self.name)) 442 | 443 | if os.path.exists(directory) and os.path.isdir(directory): 444 | setattr(Configuration, self.name, directory) 445 | self.ui.setText(directory) 446 | 447 | def updatePath(self, directory): 448 | if os.path.exists(directory) and os.path.isdir(directory): 449 | setattr(Configuration, self.name, directory) 450 | 451 | class SettingsWidget(QtWidgets.QWidget): 452 | def __init__(self, parent=None): 453 | QtWidgets.QWidget.__init__(self, parent) 454 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 455 | 456 | self.ui = Ui_SettingsWidget() 457 | self.ui.setupUi(self) 458 | 459 | self.elements = [] 460 | 461 | self.elements.append(CSpinBox("day_averaging_frames", self.ui)) 462 | self.elements.append(CSpinBox("night_averaging_frames", self.ui)) 463 | self.elements.append(CSpinBox("day_time_between_frames", self.ui)) 464 | self.elements.append(CSpinBox("night_time_between_frames", self.ui)) 465 | self.elements.append(CPathBox("default_storage_path", self.ui, self.ui.btStorage)) 466 | self.elements.append(CCheckBox("store_in_subdirectory", self.ui)) 467 | self.elements.append(CCheckBox("dnfa_enabled", self.ui)) 468 | self.elements.append(CSpinBox("dnfa_window_size", self.ui)) 469 | self.elements.append(CSpinBox("dnfa_min_med_diff_factor", self.ui)) 470 | self.elements.append(CSpinBox("dnfa_min_diff_value", self.ui)) 471 | self.elements.append(CSpinBox("dnfa_min_frames", self.ui)) 472 | self.elements.append(CSpinBox("dnfa_max_frames", self.ui)) 473 | 474 | self.elements.append(CSpinBox("rb_difference_threshold", self.ui)) 475 | self.elements.append(CSpinBox("rb_ratio_threshold", self.ui)) 476 | self.elements.append(CSpinBox("br_ratio_threshold", self.ui)) 477 | self.elements.append(CSpinBox("nbr_threshold", self.ui)) 478 | self.elements.append(CSpinBox("adaptive_threshold", self.ui)) 479 | self.elements.append(CSpinBox("adaptive_block_size", self.ui)) 480 | self.elements.append(CSpinBox("background_rect_size", self.ui)) 481 | self.elements.append(CSpinBox("mc_rb_threshold", self.ui)) 482 | self.elements.append(CSpinBox("mc_bg_threshold", self.ui)) 483 | self.elements.append(CSpinBox("mc_b_threshold", self.ui)) 484 | self.elements.append(CSpinBox("sp_num_superpixels", self.ui)) 485 | self.elements.append(CSpinBox("sp_prior", self.ui)) 486 | self.elements.append(CSpinBox("sp_num_levels", self.ui)) 487 | self.elements.append(CSpinBox("sp_num_histogram_bins", self.ui)) 488 | self.elements.append(CSpinBox("sp_num_iterations", self.ui)) 489 | self.elements.append(CSpinBox("sp_kernel_size", self.ui)) 490 | self.elements.append(CSpinBox("morphology_kernel_size", self.ui)) 491 | self.elements.append(CSpinBox("morphology_iterations", self.ui)) 492 | 493 | self.elements.append(CSpinBox("min_alt", self.ui)) 494 | self.elements.append(CSpinBox("alt_step", self.ui)) 495 | self.elements.append(CSpinBox("az_step", self.ui)) 496 | self.elements.append(CSpinBox("max_mag", self.ui)) 497 | self.elements.append(CSpinBox("gaussian_roi_size", self.ui)) 498 | self.elements.append(CSpinBox("gaussian_threshold", self.ui)) 499 | self.elements.append(CSpinBox("gaussian_kernel_size", self.ui)) 500 | self.elements.append(CSpinBox("candidate_radius", self.ui)) 501 | self.elements.append(CSpinBox("gftt_max_corners", self.ui)) 502 | self.elements.append(CSpinBox("gftt_quality_level", self.ui)) 503 | self.elements.append(CSpinBox("gftt_min_distance", self.ui)) 504 | self.elements.append(CSpinBox("surf_threshold", self.ui)) 505 | self.elements.append(CSpinBox("log_max_rect_size", self.ui)) 506 | self.elements.append(CSpinBox("log_block_size", self.ui)) 507 | self.elements.append(CSpinBox("log_threshold", self.ui)) 508 | self.elements.append(CSpinBox("log_kernel_size", self.ui)) 509 | 510 | def saveSettings(self): 511 | dictionary = {} 512 | for element in self.elements: 513 | name = element.name 514 | dictionary[name] = getattr(Configuration, name) 515 | 516 | with open(Configuration.configuration_file, 'wb') as f: 517 | pickle.dump(dictionary, f) 518 | 519 | def closeEvent(self, event): 520 | button = QtWidgets.QMessageBox.question(self, "Closing settings", "Save changes?", QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel) 521 | 522 | if button == QtWidgets.QMessageBox.Save: 523 | self.saveSettings() 524 | elif button == QtWidgets.QMessageBox.Cancel: 525 | event.ignore() 526 | return 527 | 528 | event.accept() 529 | 530 | if __name__ == "__main__": 531 | app = QtWidgets.QApplication(sys.argv) 532 | main_window = MainQT5() 533 | main_window.show() 534 | sys.exit(app.exec_()) 535 | 536 | -------------------------------------------------------------------------------- /main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1024 10 | 768 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Nephoscope 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::ScrollBarAsNeeded 34 | 35 | 36 | Qt::ScrollBarAsNeeded 37 | 38 | 39 | QMdiArea::SubWindowView 40 | 41 | 42 | false 43 | 44 | 45 | false 46 | 47 | 48 | false 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 0 58 | 0 59 | 1024 60 | 29 61 | 62 | 63 | 64 | 65 | &File 66 | 67 | 68 | 69 | 70 | 71 | 72 | &View 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | &Quit 86 | 87 | 88 | Ctrl+Q 89 | 90 | 91 | 92 | 93 | true 94 | 95 | 96 | &Live 97 | 98 | 99 | 100 | 101 | &Calibration 102 | 103 | 104 | 105 | 106 | &Settings 107 | 108 | 109 | 110 | 111 | &New View 112 | 113 | 114 | Ctrl+N 115 | 116 | 117 | 118 | 119 | &Tile 120 | 121 | 122 | 123 | 124 | 125 | 126 | action_Quit 127 | triggered() 128 | MainWindow 129 | close() 130 | 131 | 132 | -1 133 | -1 134 | 135 | 136 | 568 137 | 323 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /moon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import sys 22 | from astropy.coordinates import EarthLocation 23 | from astropy import units as u 24 | from astropy.time import Time 25 | from sky import SkyCatalog 26 | from configuration import Configuration 27 | from skycamerafile import SkyCameraFile 28 | from glob import glob 29 | 30 | class MoonChecker: 31 | def __init__(self): 32 | location = EarthLocation(lat=Configuration.latitude, lon=Configuration.longitude, height=Configuration.elevation) 33 | 34 | self.catalog = SkyCatalog(False, True) 35 | self.catalog.setLocation(location) 36 | self.now() 37 | 38 | def now(self): 39 | self.setTime(Time.now()) 40 | 41 | def setTime(self, time): 42 | self.catalog.setTime(time) 43 | _, _, alt, _ = self.catalog.calculate() 44 | 45 | self.alt = alt[0] 46 | 47 | return self.alt 48 | 49 | def isUp(self): 50 | return self.alt > Configuration.moon_up_angle 51 | 52 | def __str__(self): 53 | if self.isUp(): 54 | return "Up" 55 | else: 56 | return "Down" 57 | 58 | if __name__ == '__main__': 59 | checker = MoonChecker() 60 | 61 | if len(sys.argv) > 1: 62 | files = SkyCameraFile.glob(sys.argv[1]) 63 | 64 | for f in files: 65 | time = SkyCameraFile.parseTime(f) 66 | checker.setTime(time) 67 | print(f, checker) 68 | else: 69 | print(checker) 70 | -------------------------------------------------------------------------------- /night.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import sys 22 | from astropy.coordinates import EarthLocation 23 | from astropy import units as u 24 | from astropy.time import Time 25 | from sky import SkyCatalog 26 | from configuration import Configuration 27 | from skycamerafile import SkyCameraFile 28 | from glob import glob 29 | 30 | class NightChecker: 31 | def __init__(self): 32 | location = EarthLocation(lat=Configuration.latitude, lon=Configuration.longitude, height=Configuration.elevation) 33 | 34 | self.catalog = SkyCatalog(True) 35 | self.catalog.setLocation(location) 36 | self.now() 37 | 38 | def now(self): 39 | self.setTime(Time.now()) 40 | 41 | def setTime(self, time): 42 | self.catalog.setTime(time) 43 | _, _, alt, _ = self.catalog.calculate() 44 | 45 | self.alt = alt[0] 46 | 47 | return self.alt 48 | 49 | def isNight(self): 50 | return self.alt < Configuration.night_angle 51 | 52 | def isDay(self): 53 | return self.alt > Configuration.day_angle 54 | 55 | def __str__(self): 56 | if self.isNight(): 57 | return "Night" 58 | elif self.isDay(): 59 | return "Day" 60 | else: 61 | return "Twilight" 62 | 63 | if __name__ == '__main__': 64 | checker = NightChecker() 65 | 66 | if len(sys.argv) > 1: 67 | files = SkyCameraFile.glob(sys.argv[1]) 68 | 69 | for f in files: 70 | time = SkyCameraFile.parseTime(f) 71 | checker.setTime(time) 72 | print(f, checker) 73 | else: 74 | print(checker) 75 | -------------------------------------------------------------------------------- /settings_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 625 10 | 608 11 | 12 | 13 | 14 | Settings 15 | 16 | 17 | 18 | 19 | 20 | true 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | Cloud Detection 28 | 29 | 30 | 31 | 32 | 33 | Qt::Horizontal 34 | 35 | 36 | 37 | 38 | 39 | 40 | Red/Blue Difference 41 | 42 | 43 | 44 | 45 | 46 | &Threshold 47 | 48 | 49 | rb_difference_threshold 50 | 51 | 52 | 53 | 54 | 55 | 56 | -255 57 | 58 | 59 | 255 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Red/Blue Ratio 70 | 71 | 72 | 73 | 74 | 75 | &Threshold 76 | 77 | 78 | rb_ratio_threshold 79 | 80 | 81 | 82 | 83 | 84 | 85 | 3 86 | 87 | 88 | 0.001000000000000 89 | 90 | 91 | 5.000000000000000 92 | 93 | 94 | 0.100000000000000 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Blue/Red Ratio 105 | 106 | 107 | 108 | 109 | 110 | &Threshold 111 | 112 | 113 | br_ratio_threshold 114 | 115 | 116 | 117 | 118 | 119 | 120 | 3 121 | 122 | 123 | 0.001000000000000 124 | 125 | 126 | 5.000000000000000 127 | 128 | 129 | 0.100000000000000 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Normalized Blue/Red Ratio 140 | 141 | 142 | 143 | 144 | 145 | &Threshold 146 | 147 | 148 | nbr_threshold 149 | 150 | 151 | 152 | 153 | 154 | 155 | 3 156 | 157 | 158 | -5.000000000000000 159 | 160 | 161 | 5.000000000000000 162 | 163 | 164 | 0.010000000000000 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | Adaptive Threshold 175 | 176 | 177 | 178 | 179 | 180 | &Threshold 181 | 182 | 183 | adaptive_threshold 184 | 185 | 186 | 187 | 188 | 189 | 190 | -255 191 | 192 | 193 | 255 194 | 195 | 196 | 197 | 198 | 199 | 200 | Block Si&ze 201 | 202 | 203 | adaptive_block_size 204 | 205 | 206 | 207 | 208 | 209 | 210 | pixel 211 | 212 | 213 | 1 214 | 215 | 216 | 511 217 | 218 | 219 | 2 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | Morphology 230 | 231 | 232 | 233 | 234 | 235 | &Kernel Size 236 | 237 | 238 | morphology_kernel_size 239 | 240 | 241 | 242 | 243 | 244 | 245 | pixel 246 | 247 | 248 | 1 249 | 250 | 251 | 511 252 | 253 | 254 | 2 255 | 256 | 257 | 258 | 259 | 260 | 261 | &Iterations 262 | 263 | 264 | morphology_iterations 265 | 266 | 267 | 268 | 269 | 270 | 271 | 1 272 | 273 | 274 | 100 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | Background Subtraction 289 | 290 | 291 | 292 | 293 | 294 | &Rect Size 295 | 296 | 297 | background_rect_size 298 | 299 | 300 | 301 | 302 | 303 | 304 | pixel 305 | 306 | 307 | 1 308 | 309 | 310 | 511 311 | 312 | 313 | 2 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | Multicolor Criterion 324 | 325 | 326 | 327 | 328 | 329 | &R-B Threshold 330 | 331 | 332 | mc_rb_threshold 333 | 334 | 335 | 336 | 337 | 338 | 339 | B-&G Threshold 340 | 341 | 342 | mc_bg_threshold 343 | 344 | 345 | 346 | 347 | 348 | 349 | &B Threshold 350 | 351 | 352 | mc_b_threshold 353 | 354 | 355 | 356 | 357 | 358 | 359 | 256 360 | 361 | 362 | 363 | 364 | 365 | 366 | -255 367 | 368 | 369 | 255 370 | 371 | 372 | 5 373 | 374 | 375 | 376 | 377 | 378 | 379 | -255 380 | 381 | 382 | 255 383 | 384 | 385 | 5 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | Superpixel Segmentation 396 | 397 | 398 | 399 | 400 | 401 | Superpi&xels 402 | 403 | 404 | sp_num_superpixels 405 | 406 | 407 | 408 | 409 | 410 | 411 | 2 412 | 413 | 414 | 1000 415 | 416 | 417 | 10 418 | 419 | 420 | 421 | 422 | 423 | 424 | &Prior 425 | 426 | 427 | sp_prior 428 | 429 | 430 | 431 | 432 | 433 | 434 | 5 435 | 436 | 437 | 438 | 439 | 440 | 441 | &Levels 442 | 443 | 444 | sp_num_levels 445 | 446 | 447 | 448 | 449 | 450 | 451 | 1 452 | 453 | 454 | 455 | 456 | 457 | 458 | &Histogram Bins 459 | 460 | 461 | sp_num_histogram_bins 462 | 463 | 464 | 465 | 466 | 467 | 468 | 2 469 | 470 | 471 | 472 | 473 | 474 | 475 | &Iterations 476 | 477 | 478 | sp_num_iterations 479 | 480 | 481 | 482 | 483 | 484 | 485 | 1 486 | 487 | 488 | 489 | 490 | 491 | 492 | &Kernel Size 493 | 494 | 495 | sp_kernel_size 496 | 497 | 498 | 499 | 500 | 501 | 502 | pixel 503 | 504 | 505 | 1 506 | 507 | 508 | 511 509 | 510 | 511 | 2 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | Star Detection 527 | 528 | 529 | 530 | 531 | 532 | Qt::Horizontal 533 | 534 | 535 | 536 | 537 | 538 | 539 | Catalog Selection 540 | 541 | 542 | 543 | 544 | 545 | &Minimum Altitude 546 | 547 | 548 | min_alt 549 | 550 | 551 | 552 | 553 | 554 | 555 | ° 556 | 557 | 558 | -90 559 | 560 | 561 | 90 562 | 563 | 564 | 565 | 566 | 567 | 568 | Altitude Ste&p 569 | 570 | 571 | alt_step 572 | 573 | 574 | 575 | 576 | 577 | 578 | ° 579 | 580 | 581 | 1 582 | 583 | 584 | 90 585 | 586 | 587 | 10 588 | 589 | 590 | 591 | 592 | 593 | 594 | A&zimuth Step 595 | 596 | 597 | az_step 598 | 599 | 600 | 601 | 602 | 603 | 604 | ° 605 | 606 | 607 | 1 608 | 609 | 610 | 90 611 | 612 | 613 | 10 614 | 615 | 616 | 617 | 618 | 619 | 620 | Ma&ximum Magnitude 621 | 622 | 623 | max_mag 624 | 625 | 626 | 627 | 628 | 629 | 630 | mag 631 | 632 | 633 | 1 634 | 635 | 636 | 5.000000000000000 637 | 638 | 639 | 0.100000000000000 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | Gaussian Star Finder 650 | 651 | 652 | 653 | 654 | 655 | &ROI Size 656 | 657 | 658 | gaussian_roi_size 659 | 660 | 661 | 662 | 663 | 664 | 665 | pixel 666 | 667 | 668 | 1 669 | 670 | 671 | 2 672 | 673 | 674 | 675 | 676 | 677 | 678 | &Threshold 679 | 680 | 681 | gaussian_threshold 682 | 683 | 684 | 685 | 686 | 687 | 688 | 100.000000000000000 689 | 690 | 691 | 0.100000000000000 692 | 693 | 694 | 695 | 696 | 697 | 698 | &Kernel Size 699 | 700 | 701 | gaussian_kernel_size 702 | 703 | 704 | 705 | 706 | 707 | 708 | pixel 709 | 710 | 711 | 1 712 | 713 | 714 | 511 715 | 716 | 717 | 2 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | Candidate Star Finder 728 | 729 | 730 | 731 | 732 | 733 | &Candidate Radius 734 | 735 | 736 | candidate_radius 737 | 738 | 739 | 740 | 741 | 742 | 743 | pixel 744 | 745 | 746 | 1 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | GFTT Detector 761 | 762 | 763 | 764 | 765 | 766 | &Maximum Corners 767 | 768 | 769 | gftt_max_corners 770 | 771 | 772 | 773 | 774 | 775 | 776 | corners 777 | 778 | 779 | 1 780 | 781 | 782 | 1000 783 | 784 | 785 | 25 786 | 787 | 788 | 789 | 790 | 791 | 792 | &Quality Level 793 | 794 | 795 | gftt_quality_level 796 | 797 | 798 | 799 | 800 | 801 | 802 | 3 803 | 804 | 805 | 1.000000000000000 806 | 807 | 808 | 0.001000000000000 809 | 810 | 811 | 812 | 813 | 814 | 815 | &Minimum Distance 816 | 817 | 818 | gftt_min_distance 819 | 820 | 821 | 822 | 823 | 824 | 825 | pixel 826 | 827 | 828 | 1 829 | 830 | 831 | 511 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | SURF Detector 842 | 843 | 844 | 845 | 846 | 847 | &Threshold 848 | 849 | 850 | surf_threshold 851 | 852 | 853 | 854 | 855 | 856 | 857 | 5.000000000000000 858 | 859 | 860 | 0.100000000000000 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | LoG Detector 871 | 872 | 873 | 874 | 875 | 876 | &Maximum Rect Size 877 | 878 | 879 | log_max_rect_size 880 | 881 | 882 | 883 | 884 | 885 | 886 | pixel 887 | 888 | 889 | 1 890 | 891 | 892 | 511 893 | 894 | 895 | 896 | 897 | 898 | 899 | B&lock Size 900 | 901 | 902 | log_block_size 903 | 904 | 905 | 906 | 907 | 908 | 909 | pixel 910 | 911 | 912 | 1 913 | 914 | 915 | 511 916 | 917 | 918 | 2 919 | 920 | 921 | 922 | 923 | 924 | 925 | &Threshold 926 | 927 | 928 | log_threshold 929 | 930 | 931 | 932 | 933 | 934 | 935 | 1.000000000000000 936 | 937 | 938 | 0.010000000000000 939 | 940 | 941 | 942 | 943 | 944 | 945 | &Kernel Size 946 | 947 | 948 | log_kernel_size 949 | 950 | 951 | 952 | 953 | 954 | 955 | pixel 956 | 957 | 958 | 1 959 | 960 | 961 | 511 962 | 963 | 964 | 2 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | false 980 | 981 | 982 | Recording 983 | 984 | 985 | 986 | 987 | 988 | Qt::Horizontal 989 | 990 | 991 | 992 | 993 | 994 | 995 | Daytime 996 | 997 | 998 | 999 | 1000 | 1001 | A&veraging Frames 1002 | 1003 | 1004 | day_averaging_frames 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | frame(s) 1012 | 1013 | 1014 | 1 1015 | 1016 | 1017 | 1000 1018 | 1019 | 1020 | 10 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | &Time Between Frames 1028 | 1029 | 1030 | day_time_between_frames 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | s 1038 | 1039 | 1040 | 100 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | Storage 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | Storage &Path 1057 | 1058 | 1059 | btStorage 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | ... 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | St&ore in Subdirectories 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | Nighttime 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | Avera&ging Frames 1097 | 1098 | 1099 | night_averaging_frames 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | frame(s) 1107 | 1108 | 1109 | 1 1110 | 1111 | 1112 | 1000 1113 | 1114 | 1115 | 10 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | &Time Between Frames 1123 | 1124 | 1125 | night_time_between_frames 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | s 1133 | 1134 | 1135 | 100 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | Dynamic Night Frame Averaging 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | &Enabled 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | Window Si&ze 1159 | 1160 | 1161 | dnfa_window_size 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | frames 1169 | 1170 | 1171 | 5 1172 | 1173 | 1174 | 500 1175 | 1176 | 1177 | 10 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | &Min./Med. Difference Factor 1185 | 1186 | 1187 | dnfa_min_med_diff_factor 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 0.010000000000000 1195 | 1196 | 1197 | 5.000000000000000 1198 | 1199 | 1200 | 0.100000000000000 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | Min. Difference Va&lue 1208 | 1209 | 1210 | dnfa_min_diff_value 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 0.010000000000000 1218 | 1219 | 1220 | 5.000000000000000 1221 | 1222 | 1223 | 0.100000000000000 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | Minim&um Frames 1231 | 1232 | 1233 | dnfa_min_frames 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | frame(s) 1241 | 1242 | 1243 | 1 1244 | 1245 | 1246 | 1000 1247 | 1248 | 1249 | 10 1250 | 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | Ma&ximum Frames 1257 | 1258 | 1259 | dnfa_max_frames 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | frame(s) 1267 | 1268 | 1269 | 1 1270 | 1271 | 1272 | 1000 1273 | 1274 | 1275 | 10 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | tabWidget 1294 | day_averaging_frames 1295 | day_time_between_frames 1296 | night_averaging_frames 1297 | night_time_between_frames 1298 | default_storage_path 1299 | btStorage 1300 | store_in_subdirectory 1301 | dnfa_enabled 1302 | dnfa_window_size 1303 | dnfa_min_med_diff_factor 1304 | dnfa_min_diff_value 1305 | dnfa_min_frames 1306 | dnfa_max_frames 1307 | rb_difference_threshold 1308 | rb_ratio_threshold 1309 | br_ratio_threshold 1310 | adaptive_threshold 1311 | adaptive_block_size 1312 | morphology_kernel_size 1313 | morphology_iterations 1314 | background_rect_size 1315 | mc_rb_threshold 1316 | mc_bg_threshold 1317 | mc_b_threshold 1318 | sp_num_superpixels 1319 | sp_prior 1320 | sp_num_levels 1321 | sp_num_histogram_bins 1322 | sp_num_iterations 1323 | sp_kernel_size 1324 | min_alt 1325 | alt_step 1326 | az_step 1327 | max_mag 1328 | gaussian_roi_size 1329 | gaussian_threshold 1330 | gaussian_kernel_size 1331 | candidate_radius 1332 | gftt_max_corners 1333 | gftt_quality_level 1334 | gftt_min_distance 1335 | surf_threshold 1336 | log_max_rect_size 1337 | log_block_size 1338 | log_threshold 1339 | log_kernel_size 1340 | 1341 | 1342 | 1343 | 1344 | -------------------------------------------------------------------------------- /sky.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import numpy as np 22 | import cv2 23 | import ephem 24 | from astropy import units as u 25 | from astropy.time import Time 26 | from astropy.io import ascii 27 | from astropy.coordinates import SkyCoord, EarthLocation, AltAz 28 | from astropy.coordinates import Latitude, Longitude 29 | from astropy.table import Column 30 | from skycamerafile import SkyCameraFile 31 | from configuration import Configuration 32 | 33 | class SkyCatalog: 34 | def __init__(self, sun_only = False, moon_only = False): 35 | if sun_only: 36 | self.ephemerides = [ephem.Sun()] 37 | self.data = None 38 | elif moon_only: 39 | self.ephemerides = [ephem.Moon()] 40 | self.data = None 41 | else: 42 | self.ephemerides = [ephem.Venus(), ephem.Mars(), ephem.Jupiter(), ephem.Saturn(), ephem.Moon(), ephem.Sun()] 43 | 44 | self.data = ascii.read(Configuration.star_catalog_file, guess=False, format='fixed_width_no_header', names=('HR', 'Name', 'DM', 'HD', 'SAO', 'FK5', 'IRflag', 'r_IRflag', 'Multiple', 'ADS', 'ADScomp', 'VarID', 'RAh1900', 'RAm1900', 'RAs1900', 'DE-1900', 'DEd1900', 'DEm1900', 'DEs1900', 'RAh', 'RAm', 'RAs', 'DE-', 'DEd', 'DEm', 'DEs', 'GLON', 'GLAT', 'Vmag', 'n_Vmag', 'u_Vmag', 'B-V', 'u_B-V', 'U-B', 'u_U-B', 'R-I', 'n_R-I', 'SpType', 'n_SpType', 'pmRA', 'pmDE', 'n_Parallax', 'Parallax', 'RadVel', 'n_RadVel', 'l_RotVel', 'RotVel', 'u_RotVel', 'Dmag', 'Sep', 'MultID', 'MultCnt', 'NoteFlag'), col_starts=(0, 4, 14, 25, 31, 37, 41, 42, 43, 44, 49, 51, 60, 62, 64, 68, 69, 71, 73, 75, 77, 79, 83, 84, 86, 88, 90, 96, 102, 107, 108, 109, 114, 115, 120, 121, 126, 127, 147, 148, 154, 160, 161, 166, 170, 174, 176, 179, 180, 184, 190, 194, 196), col_ends=(3, 13, 24, 30, 36, 40, 41, 42, 43, 48, 50, 59, 61, 63, 67, 68, 70, 72, 74, 76, 78, 82, 83, 85, 87, 89, 95, 101, 106, 107, 108, 113, 114, 119, 120, 125, 126, 146, 147, 153, 159, 160, 165, 169, 173, 175, 178, 179, 183, 189, 193, 195, 196)) 45 | 46 | # removed masked rows 47 | 48 | self.data = self.data[:][~np.ma.getmaskarray(self.data['DE-'])] 49 | 50 | def setLocation(self, location): 51 | self.location = location 52 | 53 | def setTime(self, time): 54 | self.time = time 55 | 56 | def calculate(self): 57 | ephem_location = ephem.Observer() 58 | ephem_location.lat = self.location.latitude.to(u.rad) / u.rad 59 | ephem_location.lon = self.location.longitude.to(u.rad) / u.rad 60 | ephem_location.elevation = self.location.height / u.meter 61 | ephem_location.date = ephem.Date(self.time.datetime) 62 | 63 | if self.data is None: 64 | self.alt = Latitude([], unit=u.deg) 65 | self.az = Longitude([], unit=u.deg) 66 | self.names = Column([], dtype=np.str) 67 | self.vmag = Column([]) 68 | else: 69 | ra = Longitude((self.data['RAh'], self.data['RAm'], self.data['RAs']), u.h) 70 | dec = Latitude((np.core.defchararray.add(self.data['DE-'], self.data['DEd'].astype(str)).astype(int), self.data['DEm'], self.data['DEs']), u.deg) 71 | c = SkyCoord(ra, dec, frame='icrs') 72 | altaz = c.transform_to(AltAz(obstime=self.time, location=self.location)) 73 | self.alt = altaz.alt 74 | self.az = altaz.az 75 | 76 | self.names = self.data['Name'] 77 | self.vmag = self.data['Vmag'] 78 | 79 | for ephemeris in self.ephemerides: 80 | ephemeris.compute(ephem_location) 81 | self.vmag = self.vmag.insert(0, ephemeris.mag) 82 | self.alt = self.alt.insert(0, (ephemeris.alt.znorm * u.rad).to(u.deg)) 83 | self.az = self.az.insert(0, (ephemeris.az * u.rad).to(u.deg)) 84 | self.names = self.names.insert(0, ephemeris.name) 85 | 86 | return self.names, self.vmag, self.alt, self.az 87 | 88 | def filter(self, min_alt, max_mag): 89 | show = self.alt >= min_alt 90 | 91 | names = self.names[show] 92 | vmag = self.vmag[show] 93 | alt = self.alt[show] 94 | az = self.az[show] 95 | 96 | show_mags = vmag < max_mag 97 | 98 | names = names[show_mags] 99 | vmag = vmag[show_mags] 100 | alt = alt[show_mags] 101 | az = az[show_mags] 102 | 103 | return names, vmag, alt, az 104 | 105 | class SkyRenderer: 106 | def __init__(self, size): 107 | self.size = size 108 | self.image = None 109 | self.font = cv2.FONT_HERSHEY_SIMPLEX 110 | 111 | def renderCatalog(self, catalog, max_mag): 112 | self.names, self.vmag, self.alt, self.az = catalog.filter(0, max_mag) 113 | return self.render() 114 | 115 | def altazToPos(self, altaz): 116 | if not isinstance(altaz, np.ndarray): 117 | altaz = np.array([a.radian for a in altaz]) 118 | 119 | if len(altaz.shape) == 1: 120 | altaz = np.array([altaz]) 121 | 122 | r = (1 - altaz[:, 0] / (np.pi / 2)) * self.size / 2 123 | 124 | pos = np.ones(altaz.shape) 125 | 126 | pos[:, 0] = r * np.cos(-np.pi / 2 - altaz[:, 1]) + self.size / 2 127 | pos[:, 1] = r * np.sin(-np.pi / 2 - altaz[:, 1]) + self.size / 2 128 | 129 | return pos 130 | 131 | def render(self): 132 | self.image = np.zeros((self.size, self.size, 1), np.uint8) 133 | 134 | pos = self.altazToPos(np.column_stack((self.alt.radian, self.az.radian))) 135 | self.x = pos[:, 0] 136 | self.y = pos[:, 1] 137 | 138 | for i in range(len(self.names)): 139 | cv2.circle(self.image, (int(self.x[i]), int(self.y[i])), int(7 - self.vmag[i]), 255, -1) 140 | 141 | if self.vmag[i] < 1 and not np.ma.is_masked(self.names[i]): 142 | cv2.putText(self.image, self.names[i],(int(self.x[i]) + 6, int(self.y[i])), self.font, 1, 150) 143 | 144 | return self.image 145 | 146 | def findStar(self, x, y, radius): 147 | r2 = radius * radius 148 | 149 | a = np.subtract(self.x, x) 150 | b = np.subtract(self.y, y) 151 | c = a * a + b * b 152 | 153 | index = c.argmin() 154 | if c[index] < radius * radius: 155 | return (self.alt[index], self.az[index], self.names[index]) 156 | 157 | return None 158 | 159 | def highlightStar(self, image, altaz, radius, color): 160 | pos = self.altazToPos(altaz)[0] 161 | 162 | pos = (int(pos[0]), int(pos[1])) 163 | 164 | cv2.circle(image, pos, radius, color) 165 | 166 | if __name__ == '__main__': 167 | import sys 168 | window = 'Stars' 169 | size = 1024 170 | 171 | location = EarthLocation(lat=Configuration.latitude, lon=Configuration.longitude, height=Configuration.elevation) 172 | time = Time.now() 173 | 174 | if len(sys.argv) >= 2: 175 | try: 176 | time = SkyCameraFile.parseTime(sys.argv[1]) 177 | except: 178 | pass 179 | 180 | catalog = SkyCatalog() 181 | catalog.setLocation(location) 182 | catalog.setTime(time) 183 | catalog.calculate() 184 | 185 | renderer = SkyRenderer(size) 186 | image = renderer.renderCatalog(catalog, 5) 187 | 188 | cv2.namedWindow(window, cv2.WINDOW_AUTOSIZE) 189 | cv2.imshow(window, image) 190 | cv2.waitKey(0) 191 | cv2.destroyAllWindows() 192 | -------------------------------------------------------------------------------- /skycamera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | from astropy.time import Time 22 | import os 23 | import cv2 24 | import numpy as np 25 | import sys 26 | from configuration import Configuration 27 | from night import NightChecker 28 | from control import SkyCameraControl 29 | from skycamerafile import SkyCameraFile 30 | 31 | class DynamicDifferenceThreshold: 32 | def __init__(self): 33 | self.last = [] 34 | self.last_index = None 35 | self.count = 0 36 | 37 | def check(self, difference): 38 | k = Configuration.dnfa_window_size 39 | 40 | # first lets save the new value 41 | if len(self.last) < k: 42 | self.last.append(difference) 43 | self.last_index = len(self.last) - 1 44 | else: 45 | self.last_index = (self.last_index + 1) % k 46 | self.last[self.last_index] = difference 47 | 48 | # checking counts 49 | self.count += 1 50 | 51 | if self.count < Configuration.dnfa_min_frames: 52 | return False 53 | 54 | if self.count >= Configuration.dnfa_max_frames: 55 | self.count = 0 56 | return True 57 | 58 | # now let's calculate the dynamic threshold 59 | mi = min(self.last) 60 | me = np.median(self.last) 61 | threshold = Configuration.dnfa_min_med_diff_factor * max(me - mi, Configuration.dnfa_min_diff_value) + me 62 | 63 | if difference > threshold: 64 | self.count = 0 65 | return True 66 | 67 | return False 68 | 69 | class SkyCamera: 70 | @staticmethod 71 | def log(message): 72 | if Configuration.logging: 73 | time = Time.now() 74 | log = open(Configuration.log_file, "a") 75 | log.write("%s: %s\n" % (time.iso, message)) 76 | log.close() 77 | 78 | @staticmethod 79 | def getBitMask(): 80 | return np.uint8(cv2.imread(Configuration.mask_file, 0) / 255) 81 | 82 | @staticmethod 83 | def getMask(): 84 | return np.float32(SkyCamera.getBitMask()) 85 | 86 | def __init__(self): 87 | self.device = 0 88 | self.channel = 0 89 | self.capture = cv2.VideoCapture() 90 | self.control = SkyCameraControl() 91 | self.night_checker = NightChecker() 92 | self.night = None 93 | self.last_image = None 94 | self.differenceChecker = DynamicDifferenceThreshold() 95 | 96 | def checkDaytime(self): 97 | self.night_checker.now() 98 | night = self.night_checker.isNight() 99 | 100 | if self.night != night: 101 | self.night = night 102 | 103 | if Configuration.control_settings: 104 | self.control.switchConfiguration(self.night, Configuration.verbose_commands) 105 | 106 | def open(self): 107 | self.capture.open(self.device) 108 | 109 | def close(self): 110 | self.capture.release() 111 | 112 | def readNight(self): 113 | sum_image = None 114 | count = 0 115 | 116 | while True: 117 | if self.capture.grab(): 118 | if cv2.__version__[0] == '3': 119 | _, image = self.capture.retrieve(flag = self.channel) 120 | else: 121 | _, image = self.capture.retrieve(channel = self.channel) 122 | 123 | image1 = np.int32(image) 124 | 125 | if self.last_image is not None: 126 | difference = image1 - self.last_image 127 | else: 128 | difference = np.array([0]) 129 | 130 | difference = float(np.sum(np.abs(difference))) / float(difference.size) 131 | 132 | if sum_image is None: 133 | sum_image = self.last_image 134 | else: 135 | sum_image += self.last_image 136 | 137 | count += 1 138 | 139 | self.last_image = image1 140 | 141 | if self.differenceChecker.check(difference): 142 | SkyCamera.log('Difference: %f %d 1' % (difference, count)) 143 | 144 | time = Time.now() 145 | 146 | self.checkDaytime() 147 | 148 | return np.uint8(sum_image / count), time 149 | else: 150 | SkyCamera.log('Difference: %f %d 0' % (difference, count)) 151 | else: 152 | return None, None 153 | 154 | def read(self): 155 | sum_image = None 156 | image = None 157 | 158 | count = Configuration.day_averaging_frames 159 | 160 | if self.night: 161 | count = Configuration.night_averaging_frames 162 | 163 | for i in range(count): 164 | if self.capture.grab(): 165 | if cv2.__version__[0] == '3': 166 | _, image = self.capture.retrieve(flag = self.channel) 167 | else: 168 | _, image = self.capture.retrieve(channel = self.channel) 169 | 170 | if sum_image is None: 171 | sum_image = np.int32(image) 172 | else: 173 | sum_image += np.int32(image) 174 | 175 | time = Time.now() 176 | 177 | self.checkDaytime() 178 | 179 | return np.uint8(sum_image / count), time 180 | 181 | def oneShot(self): 182 | self.open() 183 | 184 | image, time = self.read() 185 | 186 | self.close() 187 | 188 | return image, time 189 | 190 | @staticmethod 191 | def saveToFile(image, time, path, sub_directory = True): 192 | SkyCameraFile.stampImage(image, time) 193 | 194 | if sub_directory: 195 | path = os.path.join(path, str(int(time.mjd))) 196 | 197 | if not os.path.exists(path): 198 | os.mkdir(path) 199 | 200 | filename = SkyCameraFile.getFileName(time) 201 | 202 | path = os.path.join(path, filename) 203 | 204 | cv2.imwrite(path, image) 205 | 206 | def captureToFile(self, path, sub_directory = True): 207 | if self.capture.isOpened(): 208 | if Configuration.dnfa_enabled: 209 | if self.night: 210 | image, time = self.readNight() 211 | else: 212 | image, time = self.read() 213 | else: 214 | image, time = self.read() 215 | else: 216 | image, time = self.oneShot() 217 | 218 | if image is None: 219 | return None, None 220 | 221 | SkyCamera.saveToFile(image, time, path, sub_directory) 222 | 223 | return image, time 224 | 225 | if __name__ == '__main__': 226 | directory = Configuration.default_storage_path 227 | count = Configuration.frame_count 228 | 229 | if len(sys.argv) > 1: 230 | directory = sys.argv[1] 231 | 232 | if len(sys.argv) > 2: 233 | count = int(sys.argv[2]) 234 | 235 | window = 'All Sky Camera' 236 | 237 | camera = SkyCamera() 238 | camera.open() 239 | 240 | loop = True 241 | 242 | if Configuration.show_recorded_frames: 243 | cv2.namedWindow(window) 244 | 245 | i = 0 246 | 247 | start = Time.now() 248 | 249 | while loop: 250 | image, time = camera.captureToFile(directory, Configuration.store_in_subdirectory) 251 | 252 | if image is None: 253 | print("Error opening Device") 254 | sys.exit(1) 255 | 256 | print("stored image %d at time %s" % (i, time.iso)) 257 | 258 | i += 1 259 | 260 | if Configuration.show_recorded_frames: 261 | cv2.imshow(window, image) 262 | 263 | time_between_frames = Configuration.day_time_between_frames 264 | 265 | if camera.night: 266 | time_between_frames = Configuration.night_time_between_frames 267 | 268 | end = Time.now() 269 | 270 | deltatime = int((time_between_frames - (end - start).sec) * 1000) 271 | 272 | if deltatime < 1: 273 | deltatime = 1 274 | 275 | key = cv2.waitKey(deltatime) & 0xFF 276 | 277 | start = Time.now() 278 | 279 | if key == 27: 280 | loop = False 281 | 282 | if count > 0: 283 | count -= 1 284 | if count == 0: 285 | loop = False 286 | 287 | camera.close() 288 | 289 | if Configuration.show_recorded_frames: 290 | cv2.destroyAllWindows() 291 | -------------------------------------------------------------------------------- /skycamerafile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | from astropy.time import Time 22 | import glob 23 | import os 24 | import cv2 25 | 26 | class SkyCameraFile: 27 | @staticmethod 28 | def glob(path): 29 | files = glob.glob(os.path.join(path, "frame_[0-9][0-9][0-9][0-9][0-9]_[0-9][0-9]_[0-9][0-9]_[0-9][0-9].jpg")) 30 | files.sort() 31 | return files 32 | 33 | @staticmethod 34 | def uniqueName(path): 35 | return os.path.splitext(os.path.basename(path))[0] 36 | 37 | @staticmethod 38 | def parseTime(path): 39 | filename = SkyCameraFile.uniqueName(path) 40 | parts = filename.split('_') 41 | if parts[0] == 'frame' and len(parts) == 5: 42 | jd = int(parts[1]) 43 | hh = int(parts[2]) 44 | mm = int(parts[3]) 45 | ss = int(parts[4]) 46 | mjd = jd + (((ss / 60) + mm) / 60 + hh) / 24 47 | return Time(mjd, format='mjd', scale='utc') 48 | else: 49 | raise Exception('Invalid filename.') 50 | 51 | @staticmethod 52 | def getFileName(when): 53 | mjd = int(when.mjd) 54 | time = when.iso.split('.')[0].split(' ')[1].replace(':', '_') 55 | return "frame_%d_%s.jpg" % (mjd, time) 56 | 57 | @staticmethod 58 | def _stampText(image, text, line): 59 | font = cv2.FONT_HERSHEY_SIMPLEX 60 | font_scale = 0.55 61 | margin = 5 62 | thickness = 2 63 | color = (255, 255, 255) 64 | 65 | size = cv2.getTextSize(text, font, font_scale, thickness) 66 | 67 | text_width = size[0][0] 68 | text_height = size[0][1] 69 | line_height = text_height + size[1] + margin 70 | 71 | x = image.shape[1] - margin - text_width 72 | y = margin + size[0][1] + line * line_height 73 | 74 | cv2.putText(image, text, (x, y), font, font_scale, color, thickness) 75 | 76 | @staticmethod 77 | def stampImage(image, when): 78 | mjd = int(when.mjd) 79 | temp = when.iso.split('.')[0].split(' ') 80 | date = temp[0] 81 | time = temp[1] 82 | 83 | SkyCameraFile._stampText(image, date, 0) 84 | SkyCameraFile._stampText(image, "UT " + time, 1) 85 | SkyCameraFile._stampText(image, "MJD " + str(mjd), 2) 86 | -------------------------------------------------------------------------------- /star_checker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | from calibration import Calibration 22 | from configuration import Configuration 23 | from star_detection import GaussianStarFinder 24 | from astropy.coordinates import EarthLocation 25 | from astropy import units as u 26 | from astropy.utils.exceptions import AstropyWarning 27 | from scipy.linalg import sqrtm 28 | import numpy as np 29 | import sys 30 | import os 31 | import cv2 32 | import warnings 33 | 34 | class StarCheckerHelper: 35 | def __init__(self, calibration_file): 36 | self.calibration = Calibration() 37 | self.calibration.load(calibration_file) 38 | 39 | def prepare(self, path, star_finder): 40 | with warnings.catch_warnings(): 41 | warnings.simplefilter('ignore', AstropyWarning) 42 | self.calibration.selectImage(path) 43 | 44 | self.names, self.vmag, alt, az = self.calibration.catalog.filter(Configuration.min_alt * u.deg, Configuration.max_mag) 45 | 46 | altaz = np.array([alt.radian, az.radian]).transpose() 47 | 48 | self.pos = np.column_stack(self.calibration.project(altaz)) 49 | 50 | self.finder = star_finder 51 | 52 | self.image = cv2.imread(path) 53 | 54 | self.finder.setImage(self.image) 55 | 56 | self.altaz = np.array([alt.degree, az.degree]).transpose() 57 | 58 | def count_stars(self): 59 | min_az = 0 60 | max_az = 360 61 | min_alt = Configuration.min_alt 62 | max_alt = 90 63 | alt_step = Configuration.alt_step 64 | az_step = Configuration.az_step 65 | 66 | alt_bins = int((max_alt - min_alt) / alt_step) 67 | az_bins = int((max_az - min_az) / az_step) 68 | 69 | counts = np.zeros([alt_bins, az_bins, 4]) 70 | 71 | for alt_bin in range(alt_bins): 72 | alt = min_alt + alt_step * alt_bin 73 | for az_bin in range(az_bins): 74 | az = min_az + az_step * az_bin 75 | 76 | counts[alt_bin, az_bin, 2] = alt 77 | counts[alt_bin, az_bin, 3] = az 78 | 79 | for i in range(self.pos.shape[0]): 80 | aa = self.altaz[i] 81 | if aa[0] > alt and aa[0] <= alt + alt_step and aa[1] > az and aa[1] <= az + az_step: 82 | counts[alt_bin, az_bin, 0] += 1 83 | if self.finder.isStar(self.pos[i][0], self.pos[i][1]): 84 | counts[alt_bin, az_bin, 1] += 1 85 | 86 | return counts 87 | 88 | def get_image(self): 89 | result = self.image.copy() 90 | 91 | good_color = (0, 255, 0) 92 | bad_color = (0, 0, 255) 93 | 94 | for i in range(self.pos.shape[0]): 95 | if self.finder.isStar(self.pos[i][0], self.pos[i][1]): 96 | color = good_color 97 | else: 98 | color = bad_color 99 | cv2.circle(result, (int(self.pos[i][0]), int(self.pos[i][1])), 3, color) 100 | 101 | return result 102 | 103 | def renderStarGauss(image, cov, mu, first, scale = 5): 104 | num_circles = 3 105 | num_points = 64 106 | 107 | cov = sqrtm(cov) 108 | 109 | num = num_circles * num_points 110 | pos = np.ones((num, 2)) 111 | 112 | for c in range(num_circles): 113 | r = c + 1 114 | for p in range(num_points): 115 | angle = p / num_points * 2 * np.pi 116 | index = c * num_points + p 117 | 118 | x = r * np.cos(angle) 119 | y = r * np.sin(angle) 120 | 121 | pos[index, 0] = x * cov[0, 0] + y * cov[0, 1] + mu[0] 122 | pos[index, 1] = x * cov[1, 0] + y * cov[1, 1] + mu[1] 123 | 124 | #image = image.copy() 125 | #image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) 126 | 127 | if first: 128 | image = cv2.resize(image, (0, 0), None, scale, scale, cv2.INTER_NEAREST) 129 | 130 | for c in range(num_circles): 131 | pts = np.array(pos[c * num_points:(c + 1) * num_points, :] * scale + scale / 2, np.int32) 132 | pts = pts.reshape((-1,1,2)) 133 | cv2.polylines(image, [pts], True, (255, 0, 0)) 134 | 135 | return image 136 | 137 | if __name__ == '__main__': 138 | if len(sys.argv) < 3: 139 | print('Usage: star_checker ') 140 | sys.exit(1) 141 | 142 | roi_size = 5 143 | 144 | scale = 5 145 | circle_radius = 5 146 | transformed_color = (0, 0, 0) 147 | good_color = (0, 255, 0) 148 | bad_color = (0, 0, 255) 149 | 150 | window = 'Star Checker' 151 | 152 | cv2.namedWindow(window, cv2.WINDOW_AUTOSIZE) 153 | 154 | path = sys.argv[1] 155 | 156 | star_finder = GaussianStarFinder() 157 | 158 | helper = StarCheckerHelper(sys.argv[2]) 159 | helper.prepare(path, star_finder) 160 | 161 | altaz = helper.altaz 162 | pos = helper.pos 163 | vmag = helper.vmag 164 | 165 | counts = helper.count_stars() 166 | 167 | bigimage = helper.image 168 | gray = cv2.cvtColor(bigimage, cv2.COLOR_BGR2GRAY) 169 | 170 | first = True 171 | 172 | for alt in range(counts.shape[0]): 173 | for az in range(counts.shape[1]): 174 | c = counts[alt, az, 0:4] 175 | 176 | if c[0] != 0: 177 | print(c[2], c[3], c[1] / c[0], c[1], c[0]) 178 | else: 179 | print(c[2], c[3], c[1], c[1], c[0]) 180 | 181 | for i in range(pos.shape[0]): 182 | A, mu, cov, roi = star_finder.findStar(pos[i][0], pos[i][1]) 183 | 184 | if mu[0] == 0 and mu[1] == 0: 185 | continue 186 | 187 | bigimage = renderStarGauss(bigimage, cov, mu, first, scale) 188 | first = False 189 | 190 | cv2.circle(bigimage, tuple(np.int32((np.int32(pos[i]) + np.array((0.5, 0.5))) * scale)), int(2 * circle_radius - vmag[i]), transformed_color) 191 | 192 | (x, y) = np.int32(mu) 193 | 194 | roi2 = gray[y - roi_size:y + roi_size + 1, x - roi_size:x + roi_size + 1] 195 | 196 | if A < 0.2: 197 | color = bad_color 198 | else: 199 | color = good_color 200 | print(vmag[i], np.sum(roi), A, (np.sum(roi2) / (2 * roi_size + 1) / (2 * roi_size + 1) - np.min(roi2)) / 255, np.sum(roi2), altaz[i, 0], altaz[i, 1]) 201 | 202 | cv2.circle(bigimage, tuple(np.int32((np.array(mu) + np.array((0.5, 0.5))) * scale)), int(5 * circle_radius - 4 * vmag[i]), color) 203 | 204 | #__import__("code").interact(local=locals()) 205 | 206 | while True: 207 | cv2.imshow(window, bigimage) 208 | 209 | k = cv2.waitKey(30) & 0xFF 210 | if k == 27: 211 | break 212 | elif k == ord('s'): 213 | cv2.imwrite('out.png', bigimage) 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /star_detection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2015-2016 Joerg Hermann Mueller 5 | # 6 | # This file is part of pynephoscope. 7 | # 8 | # pynephoscope is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # pynephoscope is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with pynephoscope. If not, see . 20 | 21 | import sys 22 | import cv2 23 | import numpy as np 24 | from scipy import optimize 25 | from skycamera import SkyCamera 26 | from configuration import Configuration 27 | import time 28 | 29 | class GaussianStarFinder: 30 | def __init__(self): 31 | self.background_gaussian = None 32 | self.mask = SkyCamera.getMask() 33 | 34 | @staticmethod 35 | def gaussBivarFit(xy, *p): 36 | (x, y) = xy 37 | 38 | A, x0, y0, v1, v2, v3 = p 39 | 40 | #X, Y = np.meshgrid(x - x0, y - y0) 41 | X, Y = x - x0, y - y0 42 | 43 | Z = A * np.exp(-1 / 2 * (v1 * X ** 2 + v2 * X * Y + v3 * Y ** 2)) 44 | 45 | return Z.ravel() 46 | 47 | def setImage(self, image): 48 | self.image = self.removeBackground(image) 49 | 50 | def removeBackground(self, image): 51 | gray = np.float32(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)) / 255 52 | 53 | if self.background_gaussian is None or self.background_gaussian.shape[0] != Configuration.gaussian_kernel_size: 54 | self.background_gaussian = cv2.getGaussianKernel(Configuration.gaussian_kernel_size, -1, cv2.CV_32F) 55 | 56 | background = cv2.sepFilter2D(gray, cv2.CV_32F, self.background_gaussian, self.background_gaussian) 57 | 58 | result = gray - background 59 | 60 | result = result * self.mask 61 | 62 | mi = np.min(result) 63 | ma = np.max(result) 64 | 65 | #result = (result - mi) / (ma - mi) 66 | return result / ma 67 | 68 | def isStar(self, x, y): 69 | A, _, _, _ = self.findStar(x, y) 70 | return A >= Configuration.gaussian_threshold 71 | 72 | def findStar(self, x, y): 73 | x = int(x) 74 | y = int(y) 75 | 76 | roi_size = Configuration.gaussian_roi_size 77 | 78 | roi = self.image[y - roi_size:y + roi_size + 1, x - roi_size:x + roi_size + 1] 79 | 80 | X, Y = np.meshgrid(range(x - roi_size, x + roi_size + 1), range(y - roi_size, y + roi_size + 1)) 81 | 82 | p0 = (1, x, y, 1, 0, 1) 83 | 84 | try: 85 | popt, _ = optimize.curve_fit(self.gaussBivarFit, (X, Y), roi.ravel(), p0=p0, maxfev=10000) 86 | except Exception as e: 87 | return 0, (0, 0), np.matrix([[0, 0], [0, 0]]), roi 88 | 89 | A, x0, y0, v1, v2, v3 = popt 90 | 91 | cov = np.matrix([[v1, v2 / 2], [v2 / 2, v3]]).I 92 | mu = (x0, y0) 93 | 94 | return A, mu, cov, roi 95 | 96 | class CandidateStarFinder: 97 | def __init__(self, detector): 98 | self.detector = detector 99 | 100 | def setDetector(self, detector): 101 | self.detector = detector 102 | 103 | def setImage(self, image): 104 | self.image = image 105 | self.candidates = self.detector.detect(image) 106 | 107 | def isStar(self, x, y): 108 | for pos in self.candidates: 109 | dx = x - pos[0] 110 | dy = y - pos[1] 111 | if dx * dx + dy * dy < Configuration.candidate_radius * Configuration.candidate_radius: 112 | return True 113 | 114 | return False 115 | 116 | def drawCandidates(self, image): 117 | for pos in self.candidates: 118 | cv2.circle(image, tuple(np.int32(pos)), 3, (0, 0, 255)) 119 | 120 | class FASTStarDetector: 121 | def __init__(self): 122 | self.fast = cv2.FastFeatureDetector_create() 123 | self.mask = SkyCamera.getMask() 124 | 125 | def detect(self, image): 126 | keypoints = self.fast.detect(image, np.uint8(self.mask)) 127 | 128 | return [kp.pt for kp in keypoints] 129 | 130 | class GFTTStarDetector: 131 | def __init__(self): 132 | pass 133 | 134 | def detect(self, image): 135 | gray = np.float32(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)) 136 | 137 | corners = cv2.goodFeaturesToTrack(gray, Configuration.gftt_max_corners, Configuration.gftt_quality_level, Configuration.gftt_min_distance) 138 | 139 | return [x[0] for x in corners] 140 | 141 | class SURFStarDetector: 142 | def __init__(self): 143 | self.threshold = Configuration.surf_threshold 144 | 145 | self.surf = None 146 | 147 | self.mask = SkyCamera.getMask() 148 | 149 | def detect(self, image): 150 | if self.threshold != Configuration.surf_threshold: 151 | self.surf = None 152 | 153 | if self.surf is None: 154 | self.surf = cv2.xfeatures2d.SURF_create(self.threshold) 155 | self.surf.setUpright(True) 156 | 157 | keypoints = self.surf.detect(image, np.uint8(self.mask)) 158 | 159 | return [kp.pt for kp in keypoints] 160 | 161 | class LoGStarDetector: 162 | def __init__(self): 163 | self.gaussian = None 164 | 165 | def detect(self, image): 166 | floatimage = cv2.cvtColor(np.float32(image), cv2.COLOR_BGR2GRAY) / 255 167 | 168 | if self.gaussian is None or self.gaussian.shape[0] != Configuration.log_kernel_size: 169 | self.gaussian = cv2.getGaussianKernel(Configuration.log_kernel_size, -1, cv2.CV_32F) 170 | 171 | gaussian_filtered = cv2.sepFilter2D(floatimage, cv2.CV_32F, self.gaussian, self.gaussian) 172 | 173 | # LoG 174 | filtered = cv2.Laplacian(gaussian_filtered, cv2.CV_32F, ksize=Configuration.log_block_size) 175 | 176 | # DoG 177 | #gaussian2 = cv2.getGaussianKernel(Configuration.log_block_size, -1, cv2.CV_32F) 178 | #gaussian_filtered2 = cv2.sepFilter2D(floatimage, cv2.CV_32F, gaussian2, gaussian2) 179 | #filtered = gaussian_filtered - gaussian_filtered2 180 | 181 | mi = np.min(filtered) 182 | ma = np.max(filtered) 183 | 184 | if mi - ma != 0: 185 | filtered = 1 - (filtered - mi) / (ma - mi) 186 | 187 | _, thresholded = cv2.threshold(filtered, Configuration.log_threshold, 1.0, cv2.THRESH_BINARY) 188 | self.debug = thresholded 189 | thresholded = np.uint8(thresholded) 190 | 191 | contours = None 192 | 193 | if int(cv2.__version__.split('.')[0]) == 2: 194 | contours, _ = cv2.findContours(thresholded, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 195 | else: 196 | _, contours, _ = cv2.findContours(thresholded, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 197 | 198 | candidates = [] 199 | 200 | for i in range(len(contours)): 201 | rect = cv2.boundingRect(contours[i]) 202 | v1 = rect[0:2] 203 | v2 = np.add(rect[0:2], rect[2:4]) 204 | if rect[2] < Configuration.log_max_rect_size and rect[3] < Configuration.log_max_rect_size: 205 | roi = floatimage[v1[1]:v2[1], v1[0]:v2[0]] 206 | _, _, _, maxLoc = cv2.minMaxLoc(roi) 207 | maxLoc = np.add(maxLoc, v1) 208 | 209 | candidates.append(maxLoc) 210 | 211 | self.candidates = candidates 212 | 213 | return candidates 214 | 215 | if __name__ == '__main__': 216 | def nothing(x): 217 | pass 218 | 219 | def hist_lines(image, start, end): 220 | scale = 4 221 | height = 1080 222 | 223 | result = np.zeros((height, 256 * scale, 1)) 224 | 225 | hist = cv2.calcHist([image], [0], None, [256], [start, end]) 226 | cv2.normalize(hist, hist, 0, height, cv2.NORM_MINMAX) 227 | hist = np.int32(np.around(hist)) 228 | 229 | for x, y in enumerate(hist): 230 | cv2.rectangle(result, (x * scale, 0), ((x + 1) * scale, y), (255), -1) 231 | 232 | result = np.flipud(result) 233 | return result 234 | 235 | if len(sys.argv) < 2: 236 | print('Usage: nephoscope ') 237 | sys.exit(1) 238 | 239 | filename = sys.argv[1] 240 | 241 | print('Reading ' + filename) 242 | 243 | image = cv2.imread(filename, 1) 244 | 245 | #image = cv2.fastNlMeansDenoisingColored(image, None, 2, 2, 7, 21) 246 | 247 | window = 'Nephoscope - star detection' 248 | window2 = 'Histogram' 249 | tb_image_switch = '0: original\n1: stars\n2: debug\n3: mser' 250 | tb_kernel_size = 'Kernel size (*2 + 1)' 251 | tb_block_size = 'Block size (*2 + 1)' 252 | tb_threshold = 'threshold' 253 | 254 | #import matplotlib 255 | #matplotlib.use('agg') 256 | from matplotlib import pyplot as plt 257 | 258 | cv2.namedWindow(window, cv2.WINDOW_AUTOSIZE) 259 | 260 | cv2.createTrackbar(tb_image_switch, window, 7, 7, nothing) 261 | cv2.createTrackbar(tb_kernel_size, window, 2, 100, nothing) 262 | cv2.createTrackbar(tb_block_size, window, 4, 100, nothing) 263 | cv2.createTrackbar(tb_threshold, window, 53, 100, nothing) 264 | 265 | cv2.imshow(window, image) 266 | 267 | cv2.namedWindow(window2, cv2.WINDOW_AUTOSIZE) 268 | 269 | height = image.shape[0] 270 | width = image.shape[1] 271 | 272 | # compression makes the mask bad, so we throd away the last two bits 273 | b, g, r = cv2.split(image) 274 | saturated = np.float32(cv2.bitwise_and(cv2.bitwise_and(b, g), r) > 251) 275 | 276 | gaussian_star_finder = GaussianStarFinder() 277 | log_star_detector = LoGStarDetector() 278 | candidate_finder = CandidateStarFinder(log_star_detector) 279 | candidate_finder.setImage(image) 280 | 281 | star_detectors = {} 282 | 283 | star_detectors[1] = log_star_detector 284 | star_detectors[2] = log_star_detector 285 | star_detectors[4] = GFTTStarDetector() 286 | star_detectors[5] = SURFStarDetector() 287 | star_detectors[6] = FASTStarDetector() 288 | 289 | last_image_switch = 0 290 | 291 | result = image 292 | 293 | while True: 294 | image_switch = cv2.getTrackbarPos(tb_image_switch, window) 295 | kernel_size = cv2.getTrackbarPos(tb_kernel_size, window) * 2 + 1 296 | block_size = cv2.getTrackbarPos(tb_block_size, window) * 2 + 1 297 | threshold = cv2.getTrackbarPos(tb_threshold, window) / 100.0 298 | 299 | if image_switch != last_image_switch: 300 | if image_switch in star_detectors: 301 | candidate_finder.setDetector(star_detectors[image_switch]) 302 | candidate_finder.setImage(image) 303 | last_image_switch = image_switch 304 | 305 | if image_switch == 0: 306 | result = image 307 | elif image_switch == 3: 308 | mser = None 309 | if int(cv2.__version__.split('.')[0]) == 2: 310 | mser = cv2.MSER(1, 1, 30) 311 | else: 312 | mser = cv2.MSER_create(1, 1, 30) 313 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 314 | msers = mser.detect(gray) 315 | result = image.copy() 316 | cv2.polylines(result, msers, True, (0, 0, 255)) 317 | elif image_switch == 4: 318 | result = image.copy() 319 | 320 | candidate_finder.drawCandidates(result) 321 | elif image_switch == 5: 322 | Configuration.surf_threshold = threshold * 100 323 | 324 | candidate_finder.setImage(image) 325 | 326 | result = image.copy() 327 | 328 | candidate_finder.drawCandidates(result) 329 | elif image_switch == 6: 330 | result = image.copy() 331 | 332 | candidate_finder.drawCandidates(result) 333 | elif image_switch == 7: 334 | result = gaussian_star_finder.removeBackground(image) 335 | 336 | gaussian_size = 4 337 | sigma = 2 338 | 339 | gaussian = cv2.getGaussianKernel(gaussian_size * 2 + 1, sigma, cv2.CV_32F) 340 | 341 | final = np.outer(gaussian, gaussian) 342 | 343 | hist = hist_lines(result, 0.01, 1) 344 | 345 | cv2.imshow(window2, hist) 346 | 347 | if True: 348 | #result2 = cv2.matchTemplate(result, final, cv2.TM_SQDIFF_NORMED) 349 | #result2 = cv2.matchTemplate(result, final, cv2.TM_SQDIFF) 350 | 351 | #size = result2.shape 352 | 353 | #result = np.zeros(result.shape, np.float32) 354 | #result[gaussian_size:(gaussian_size + size[0]), gaussian_size:(gaussian_size + size[1])] = 1 - result2 355 | 356 | #result = result * gaussian_star_finder.mask 357 | 358 | #print(np.min(result)) 359 | #print(np.max(result)) 360 | 361 | #_, result = cv2.threshold(result, threshold, 1.0, cv2.THRESH_BINARY) 362 | pass 363 | else: 364 | fast = cv2.FastFeatureDetector_create() 365 | 366 | kp = fast.detect(cv2.cvtColor(result, cv2.COLOR_GRAY2BGR), np.uint8(gaussian_star_finder.mask)) 367 | 368 | result = image.copy() 369 | 370 | cv2.drawKeypoints(result, kp, result, (0,0,255), 1) 371 | else: 372 | log_star_detector.kernel_size = kernel_size 373 | log_star_detector.block_size = block_size 374 | log_star_detector.threshold = threshold 375 | 376 | candidate_finder.setImage(image) 377 | 378 | if image_switch == 2: 379 | result = log_star_detector.debug 380 | 381 | masked = cv2.multiply(result, 1 - saturated) 382 | result = cv2.multiply(masked, gaussian_star_finder.mask) * 255 383 | else: 384 | result = image.copy() 385 | candidate_finder.drawCandidates(result) 386 | 387 | cv2.imshow(window, result) 388 | 389 | 390 | k = cv2.waitKey(30) & 0xFF 391 | if k == 27: 392 | break 393 | if k == ord('s'): 394 | filename = 'out.png' 395 | print('Saving ' + filename) 396 | cv2.imwrite(filename, result) 397 | 398 | if k == ord(' '): 399 | print(np.max(result)) 400 | print(np.min(result)) 401 | 402 | #__import__("code").interact(local=locals()) 403 | 404 | cv2.destroyAllWindows() 405 | --------------------------------------------------------------------------------