├── .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 |
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 |
--------------------------------------------------------------------------------