├── LICENSE
├── README.md
├── annotator.py
├── annotator_video.py
├── app_inference_core.py
├── backup
├── annotator.py
└── annotator_video.py
├── canvas.py
├── categories.txt
├── demo.gif
├── demo_image
└── demo.jpg
├── demo_jntm.gif
├── demo_video
└── kunkun_00000_15_59
│ ├── 00000.jpg
│ ├── 00001.jpg
│ ├── 00002.jpg
│ ├── 00003.jpg
│ ├── 00004.jpg
│ ├── 00005.jpg
│ ├── 00006.jpg
│ ├── 00007.jpg
│ ├── 00008.jpg
│ ├── 00009.jpg
│ ├── 00010.jpg
│ ├── 00011.jpg
│ ├── 00012.jpg
│ ├── 00013.jpg
│ ├── 00014.jpg
│ ├── 00015.jpg
│ ├── 00016.jpg
│ ├── 00017.jpg
│ ├── 00018.jpg
│ ├── 00019.jpg
│ ├── 00020.jpg
│ ├── 00021.jpg
│ ├── 00022.jpg
│ ├── 00023.jpg
│ ├── 00024.jpg
│ ├── 00025.jpg
│ ├── 00026.jpg
│ ├── 00027.jpg
│ ├── 00028.jpg
│ ├── 00029.jpg
│ ├── 00030.jpg
│ ├── 00031.jpg
│ ├── 00032.jpg
│ ├── 00033.jpg
│ ├── 00034.jpg
│ ├── 00035.jpg
│ ├── 00036.jpg
│ ├── 00037.jpg
│ ├── 00038.jpg
│ ├── 00039.jpg
│ ├── 00040.jpg
│ ├── 00041.jpg
│ ├── 00042.jpg
│ ├── 00043.jpg
│ ├── 00044.jpg
│ └── 00045.jpg
├── mask_predictor.py
├── requirements.txt
├── shape.py
└── utils
├── .DS_Store
├── __init__.py
├── _io.py
├── download_model.py
├── image.py
├── qt.py
└── shape.py
/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 |
635 | Copyright (C)
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 | Copyright (C)
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 | # segment-anything-annotator
2 | We developed a python UI based on labelme and segment-anything for pixel-level annotation. It support generating multiple masks by SAM(box/point prompt), efficient polygon modification and category record. We will add more features (such as incorporating CLIP-based methods for category proposal and VOS methods for mask association of video datasets)
3 |
4 | Any feedback or suggestions are welcomed. We will continuously add features and fix bugs. 👀👀👀
5 |
6 |
7 | ## News
8 | `28 Apr`: Change the output format with labelme format. If you want to use the old output format, please use `backup/annotator.py`.
9 |
10 | `21 Apr`: Add annotation script for video dataset. See [Video Usage](https://github.com/haochenheheda/segment-anything-annotator#video-usage) for the details.
11 |
12 | ## Features
13 | - [x] Interactive Segmentation by SAM (both boxes and points prompt)
14 | - [x] Multiple Output Choices
15 | - [x] Category Annotation
16 | - [x] Polygon modification
17 | - [ ] CLIP for Category Proposal
18 | - [x] STCN for Video Dataset Annotation
19 |
20 | ## Demo
21 |
22 |
23 | ## Installation
24 | 1. Python>=3.8
25 | 2. [Pytorch](https://pytorch.org/)
26 | 3. pip install -r requirements.txt
27 |
28 | ## Usage
29 | ### 1. Start the Annotation Platform
30 |
31 | ```
32 | python annotator.py --app_resolution 1000,1600 --model_type vit_b --keep_input_size True --max_size 720
33 | ```
34 | `--model_type`: vit_b, vit_l, vit_h
35 |
36 | `--keep_input_size`: `True`: keep the origin image size for SAM; `False`: resize the input image to `--max_size` (save GPU memory)
37 |
38 | ### 2. Load the category list file if you want to annotate object categories.
39 | Click the `Category File` on the top tool bar and choose your own one, such as the `categories.txt` in this repo.
40 |
41 | ### 3. Specify the image and save folds
42 | Click the 'Image Directory' on the top tool bar to specify the fold containing images (in .jpg or .png).
43 | Click the 'Save Directory' on the top tool bar to specify the fold for saving the annotations. The annotations of each image will be saved as json file in the following format
44 | ```
45 | [
46 | #object1
47 | {
48 | 'label':,
49 | 'group_id':,
50 | 'shape_type':'polygon',
51 | 'points':[[x1,y1],[x2,y2],[x3,y3],...]
52 | },
53 | #object2
54 | ...
55 | ]
56 | ```
57 |
58 | ### 4. Load SAM model
59 | Click the "Load SAM" on the top tool bar to load the SAM model. The model will be automatically downloaded at the first time. Please be patient. Or you can manually download the [models](https://github.com/facebookresearch/segment-anything#model-checkpoints) and put them in the root directory named `vit_b.pth`, `vit_l.pth` and `vit_h.pth`.
60 |
61 | ### 5. Annotating Functions
62 | `Manual Polygons`: manually add masks by clicking on the boundary of the objects, just like the Labelme (Press right button and drag to draw the arcs easily).
63 |
64 | `Point Prompt`: generate mask proposals with clicks. The mouse leftpress/rightpress represent positive/negative clicks respectively.
65 | You can see several mask proposals below in the boxes: `Proposal1-4`, and you could choose one by clicking or shortcuts `1`,`2`,`3`,`4`.
66 |
67 | `Box Prompt`: generate mask proposals with boxes.
68 |
69 | `Accept`(shortcut:`a`): accept the chosen proposal and add to the annotation dock.
70 |
71 | `Reject`(shortcut:`r`): reject the proposals and clean the workspace.
72 |
73 | `Save`(shortcut:'s'): save annotations to file. Do not forget to save your annotation for each image, or it will be lost when you switch to the next image.
74 |
75 | `Edit Polygons`: in this mode, you could modify the annotated objects, such as changing the category labels or ids by double click on object items in the
76 | annotation dock. And you can modify the boundary by draging the points on the boundary.
77 |
78 | `Delete`(shortcut:'d'): under `Edit Mode`, delete selected/hightlight objects from annotation dock.
79 |
80 | `Reduce Point`: under `Edit Mode`, if you find the polygon is too dense to edit, you could use this button to reduce the points on the selected polygons. But this will slightly reduce the annotation quality.
81 |
82 | `Zoom in/out`: press 'CTRL' and scroll wheel on the mouse
83 |
84 | `Class On/Off`: if the Class is turned on, a dialog will show after you accept a mask to record category and id, or the catgeory will be default value "Object".
85 |
86 |
87 | ## Video Usage
88 | ### 1. clone [STCN](https://github.com/hkchengrex/STCN), download the [stcn.pth](https://drive.google.com/file/d/1mRrE0uCI2ktdWlUgapJI_KmgeIiF2eOm/view) and put them in the root directory like this:
89 | ```
90 | -| segment-anything-annotator
91 | -| annotation_video.py
92 | .....
93 | -| STCN
94 | -| stcn.pth
95 | ```
96 |
97 | ### 2. Start the Annotation Platform
98 | ```
99 | python annotator_video.py --app_resolution 1000,1600 --model_type vit_b --keep_input_size True --max_size 720 --max_size_STCN 600
100 | ```
101 | `--model_type`: vit_b, vit_l, vit_h
102 | `--keep_input_size`: `True`: keep the origin image size for SAM; `False`: resize the input image to `--max_size` (save GPU memory)
103 | `--max_size_STCN`: the maximum input image size for STCN (don't be too large)
104 |
105 | ### 3. Specify the video and save folds
106 | Click 'Video Directory' and 'Save Directory'.
107 | The folds containing videos should be structured like this:
108 | ```
109 | -| video_fold
110 | -| video1
111 | -| 00000.jpg
112 | -| 00001.jpg
113 | ...
114 | -| video2
115 | -| 00000.jpg
116 | -| 00001.jpg
117 | ...
118 | ```
119 | ### 3. Load STCN and SAM
120 | Click 'Load STCN' and 'Load SAM'.
121 |
122 | ### 4. Video Annotating Functions
123 | (a) Finish the annotations of the first frame with SAM
124 |
125 | (b) Press and hold `left control`, then press `left mouse button` to select the objects you want to track (should be highlighted by colors)
126 |
127 | (c) Click `add object to memory` to initialize the tracklets
128 |
129 | (d) move to next frame, and click `Propagate` to obtain tracked masks on new frame. (the results will be automatically saved when you change frames)
130 |
131 | (e) if the propagated masks are not good enough, press 'e' to enter Edit mode, then you could manually correct the masks. You could also use `Add as key frame` to add a certain frame as reference to improve the propagation stability.
132 |
133 | ShortCuts: `b`: `last frame`, `n`: `next frame`, `e`: `edit model`, `a`: `accept proposal`, `r`: `reject proposal`, `d`: `delete`, `s`: `save`, `space`: `propagate`
134 |
135 |
136 |
137 | ## To Do
138 | - [ ] CLIP for Category Proposal
139 | - [x] STCN for Video Dataset Annotation
140 | - [ ] Fix bugs and optimize the UI
141 | - [ ] Annotation Files -> Labelme Format/COCO Format/Youtube-VIS Format
142 |
143 | ## Acknowledgement
144 | This repo is built on [SAM](https://github.com/facebookresearch/segment-anything) and [Labelme]().
145 |
146 |
147 |
--------------------------------------------------------------------------------
/app_inference_core.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import sys
3 |
4 | sys.path.insert(0, 'STCN/')
5 | from inference_memory_bank import MemoryBank
6 | from model.eval_network import STCN
7 | from model.aggregate import aggregate
8 |
9 | from util.tensor_util import pad_divide_by
10 |
11 |
12 | class InferenceCore:
13 | def __init__(self, prop_net:STCN, images, num_objects, top_k=20, mem_every=5, include_last=False, device='cuda'):
14 | self.prop_net = prop_net
15 | self.mem_every = mem_every
16 | self.include_last = include_last
17 |
18 | # True dimensions
19 | t = images.shape[1]
20 | h, w = images.shape[-2:]
21 |
22 | # Pad each side to multiple of 16
23 | images, self.pad = pad_divide_by(images, 16)
24 | # Padded dimensions
25 | nh, nw = images.shape[-2:]
26 |
27 | self.images = images
28 | self.device = device
29 |
30 | self.k = num_objects
31 |
32 | # Background included, not always consistent (i.e. sum up to 1)
33 | self.prob = torch.zeros((self.k+1, t, 1, nh, nw), dtype=torch.float32, device=self.device)
34 | self.prob[0] = 1e-7
35 |
36 | self.t, self.h, self.w = t, h, w
37 | self.nh, self.nw = nh, nw
38 | self.kh = self.nh//16
39 | self.kw = self.nw//16
40 |
41 | self.mem_bank = MemoryBank(k=self.k, top_k=top_k)
42 |
43 | def encode_key(self, idx):
44 | result = self.prop_net.encode_key(self.images[:,idx].to(device=self.device))
45 | return result
46 |
47 | def do_pass(self, key_k, key_v, idx, end_idx):
48 | self.mem_bank.add_memory(key_k, key_v)
49 | closest_ti = end_idx
50 |
51 | # Note that we never reach closest_ti, just the frame before it
52 | this_range = range(idx+1, closest_ti)
53 | end = closest_ti - 1
54 |
55 | for ti in this_range:
56 | k16, qv16, qf16, qf8, qf4 = self.encode_key(ti)
57 | out_mask = self.prop_net.segment_with_query(self.mem_bank, qf8, qf4, k16, qv16)
58 |
59 | out_mask = aggregate(out_mask, keep_bg=True)
60 | self.prob[:,ti] = out_mask
61 |
62 | if ti != end:
63 | is_mem_frame = ((ti % self.mem_every) == 0)
64 | if self.include_last or is_mem_frame:
65 | prev_value = self.prop_net.encode_value(self.images[:,ti].to(device=self.device), qf16, out_mask[1:])
66 | prev_key = k16.unsqueeze(2)
67 | self.mem_bank.add_memory(prev_key, prev_value, is_temp=not is_mem_frame)
68 |
69 | return closest_ti
70 |
71 | def interact(self, mask, frame_idx, end_idx):
72 | mask, _ = pad_divide_by(mask.to(device=self.device), 16)
73 |
74 | self.prob[:, frame_idx] = aggregate(mask, keep_bg=True)
75 |
76 | # KV pair for the interacting frame
77 | key_k, _, qf16, _, _ = self.encode_key(frame_idx)
78 | key_v = self.prop_net.encode_value(self.images[:,frame_idx].to(device=self.device), qf16, self.prob[1:,frame_idx].to(device=self.device))
79 | key_k = key_k.unsqueeze(2)
80 |
81 | # Propagate
82 | self.do_pass(key_k, key_v, frame_idx, end_idx)
83 |
--------------------------------------------------------------------------------
/backup/annotator.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import functools
3 | import cv2
4 | import glob
5 | import os
6 | import os.path as osp
7 | import imgviz
8 | import html
9 | import json
10 | import math
11 | import argparse
12 | import numpy as np
13 | import tempfile
14 | import torch
15 |
16 | from PyQt5.QtWidgets import QWidget, QApplication, QMainWindow, QApplication, QPushButton, QLabel, QFileDialog, QProgressBar, QComboBox, QScrollArea, QDockWidget, QMessageBox
17 | from PyQt5.QtGui import QPixmap, QIcon, QImage
18 | from PyQt5.Qt import QSize
19 | from qtpy.QtCore import Qt
20 | from qtpy import QtCore
21 | from qtpy import QtGui, QtWidgets
22 | from canvas import Canvas
23 | import utils
24 | from utils.download_model import download_model
25 |
26 | from labelme.widgets import ToolBar, UniqueLabelQListWidget, LabelDialog, LabelListWidget, LabelListWidgetItem, ZoomWidget
27 | from labelme import PY2
28 | from labelme.label_file import LabelFile
29 | from labelme.label_file import LabelFileError
30 |
31 |
32 | from shape import Shape
33 |
34 | from PIL import Image
35 |
36 | from collections import namedtuple
37 | Click = namedtuple('Click', ['is_positive', 'coords'])
38 |
39 | from segment_anything import sam_model_registry, SamPredictor
40 |
41 |
42 |
43 |
44 |
45 | LABEL_COLORMAP = imgviz.label_colormap()
46 |
47 | class MainWindow(QMainWindow):
48 |
49 | FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = 0, 1, 2
50 |
51 | def __init__(self, parent=None, global_w=1000, global_h=1800, model_type='vit_b', keep_input_size=True, max_size=1080):
52 | super(MainWindow, self).__init__(parent)
53 | self.resize(global_w, global_h)
54 | self.model_type = model_type
55 | self.keep_input_size = keep_input_size
56 | self.max_size = float(max_size)
57 |
58 | self.setWindowTitle('segment-anything-annotator')
59 | self.canvas = Canvas(self,
60 | epsilon=10.0,
61 | double_click='close',
62 | num_backups=10,
63 | app=self,
64 | )
65 |
66 |
67 | self._noSelectionSlot = False
68 | self.current_output_dir = 'output'
69 | os.makedirs(self.current_output_dir, exist_ok=True)
70 | self.current_output_filename = ''
71 | self.canvas.zoomRequest.connect(self.zoomRequest)
72 |
73 | self.memory_shapes = []
74 | self.sam_mask = []
75 | self.sam_mask_proposal = []
76 | self.image_encoded_flag = False
77 | self.min_point_dis = 4
78 |
79 | self.predictor = None
80 |
81 | self.scroll_values = {
82 | Qt.Horizontal: {},
83 | Qt.Vertical: {},
84 | }
85 | self.scrollArea = QScrollArea(self)
86 | self.scrollArea.setWidget(self.canvas)
87 | self.scrollArea.setWidgetResizable(True)
88 | self.scrollBars = {
89 | Qt.Vertical: self.scrollArea.verticalScrollBar(),
90 | Qt.Horizontal: self.scrollArea.horizontalScrollBar(),
91 | }
92 | self.canvas.scrollRequest.connect(self.scrollRequest)
93 | self.canvas.newShape.connect(self.newShape)
94 | self.canvas.shapeMoved.connect(self.setDirty)
95 | self.canvas.selectionChanged.connect(self.shapeSelectionChanged)
96 | self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive)
97 |
98 | self.uniqLabelList = UniqueLabelQListWidget()
99 | self.uniqLabelList.setToolTip(
100 | self.tr(
101 | "Select label to start annotating for it. "
102 | "Press 'Esc' to deselect."
103 | )
104 | )
105 | self.labelDialog = LabelDialog(
106 | parent=self,
107 | labels=[],
108 | sort_labels=False,
109 | show_text_field=True,
110 | completion='contains',
111 | fit_to_content={'column': True, 'row': False},
112 | )
113 |
114 | self.labelList = LabelListWidget()
115 | self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged)
116 | self.labelList.itemDoubleClicked.connect(self.editLabel)
117 | self.labelList.itemChanged.connect(self.labelItemChanged)
118 | self.labelList.itemDropped.connect(self.labelOrderChanged)
119 |
120 | self.shape_dock = QDockWidget(
121 | self.tr("Polygon Labels"), self
122 | )
123 | self.shape_dock.setObjectName("Labels")
124 | self.shape_dock.setWidget(self.labelList)
125 |
126 | self.category_list = [i.strip() for i in open('categories.txt', 'r', encoding='utf-8').readlines()]
127 | self.labelDialog = LabelDialog(
128 | parent=self,
129 | labels=self.category_list,
130 | sort_labels=False,
131 | show_text_field=True,
132 | completion='contains',
133 | fit_to_content={'column': True, 'row': False},
134 | )
135 | self.zoom_values = {}
136 | self.video_directory = ''
137 | self.video_list = []
138 | self.video_len = len(self.video_list)
139 |
140 | self.img_list = []
141 | self.img_len = len(self.img_list)
142 | self.current_img_index = 0
143 | self.current_img = ''
144 |
145 | self.button_next = QPushButton('Next Image', self)
146 | self.button_next.clicked.connect(self.clickButtonNext)
147 | self.button_last = QPushButton('Last Image', self)
148 | self.button_last.clicked.connect(self.clickButtonLast)
149 |
150 | self.img_progress_bar = QProgressBar(self)
151 | self.img_progress_bar.setMinimum(0)
152 | self.img_progress_bar.setMaximum(1)
153 | self.img_progress_bar.setValue(0)
154 | self.button_proposal1 = QPushButton('Proposal1', self)
155 | self.button_proposal1.clicked.connect(self.choose_proposal1)
156 | self.button_proposal1.setShortcut('1')
157 | self.button_proposal2 = QPushButton('Proposal2', self)
158 | self.button_proposal2.clicked.connect(self.choose_proposal2)
159 | self.button_proposal2.setShortcut('2')
160 | self.button_proposal3 = QPushButton('Proposal3', self)
161 | self.button_proposal3.clicked.connect(self.choose_proposal3)
162 | self.button_proposal3.setShortcut('3')
163 | self.button_proposal4 = QPushButton('Proposal4', self)
164 | self.button_proposal4.clicked.connect(self.choose_proposal4)
165 | self.button_proposal4.setShortcut('4')
166 | self.button_proposal_list = [self.button_proposal1, self.button_proposal2, self.button_proposal3, self.button_proposal4]
167 |
168 | self.class_on_flag = True
169 | self.class_on_text = QLabel("Class On", self)
170 |
171 |
172 | #naive layout
173 | self.scrollArea.move(int(0.02 * global_w), int(0.08 * global_h))
174 | self.scrollArea.resize(int(0.75 * global_w), int(0.7 * global_h))
175 | self.shape_dock.move(int(0.79 * global_w), int(0.08 * global_h))
176 | self.shape_dock.resize(int(0.2 * global_w), int(0.7 * global_h))
177 | self.button_next.move(int(0.18 * global_w), int(0.85 * global_h))
178 | self.button_next.resize(int(0.1 * global_w),int(0.04 * global_h))
179 | self.button_last.move(int(0.01 * global_w), int(0.85 * global_h))
180 | self.button_last.resize(int(0.1 * global_w),int(0.04 * global_h))
181 | self.class_on_text.move(int(0.01 * global_w), int(0.9 * global_h))
182 | self.img_progress_bar.move(int(0.01 * global_w), int(0.8 * global_h))
183 | self.img_progress_bar.resize(int(0.3 * global_w),int(0.04 * global_h))
184 |
185 | self.button_proposal1.resize(int(0.17 * global_w),int(0.14 * global_h))
186 | self.button_proposal1.move(int(0.33 * global_w), int(0.8 * global_h))
187 | self.button_proposal2.resize(int(0.17 * global_w),int(0.14 * global_h))
188 | self.button_proposal2.move(int(0.50 * global_w), int(0.8 * global_h))
189 | self.button_proposal3.resize(int(0.17 * global_w),int(0.14 * global_h))
190 | self.button_proposal3.move(int(0.67 * global_w), int(0.8 * global_h))
191 | self.button_proposal4.resize(int(0.17 * global_w),int(0.14 * global_h))
192 | self.button_proposal4.move(int(0.84 * global_w), int(0.8 * global_h))
193 |
194 |
195 |
196 | self.zoomWidget = ZoomWidget()
197 |
198 | action = functools.partial(utils.newAction, self)
199 |
200 |
201 | categoryFile = action(
202 | self.tr("Category File"),
203 | lambda: self.clickCategoryChoose(),
204 | 'None',
205 | "objects",
206 | self.tr("Category File"),
207 | enabled=True,
208 | )
209 | imageDirectory = action(
210 | self.tr("Image Directory"),
211 | lambda: self.clickFileChoose(),
212 | 'None',
213 | "objects",
214 | self.tr("Image Directory"),
215 | enabled=True,
216 | )
217 | LoadSAM = action(
218 | self.tr("Load SAM"),
219 | lambda: self.clickLoadSAM(),
220 | 'None',
221 | "objects",
222 | self.tr("Load SAM"),
223 | enabled=True,
224 | )
225 | AutoSeg = action(
226 | self.tr("AutoSeg"),
227 | lambda: self.clickAutoSeg(),
228 | 'None',
229 | "objects",
230 | self.tr("AutoSeg"),
231 | enabled=False,
232 | )
233 | promptSeg = action(
234 | self.tr("Accept"),
235 | lambda: self.addSamMask(),
236 | 'a',
237 | "objects",
238 | self.tr("Accept"),
239 | enabled=False,
240 | )
241 |
242 | saveDirectory = action(
243 | self.tr("Save Directory"),
244 | lambda: self.clickSaveChoose(),
245 | 'None',
246 | "objects",
247 | self.tr("Save Directory"),
248 | enabled=True,
249 | )
250 |
251 | createMode = action(
252 | self.tr("Manual Polygons"),
253 | lambda: self.toggleDrawMode(False, createMode="polygon"),
254 | 'Ctrl+W',
255 | "objects",
256 | self.tr("Start drawing polygons"),
257 | enabled=True,
258 | )
259 | createPointMode = action(
260 | self.tr("Point Prompt"),
261 | lambda: self.toggleDrawMode(False, createMode="point"),
262 | 'None',
263 | "objects",
264 | self.tr("Point Prompt"),
265 | enabled=True,
266 | )
267 | createRectangleMode = action(
268 | self.tr("Box Prompt"),
269 | lambda: self.toggleDrawMode(False, createMode="rectangle"),
270 | 'None',
271 | "objects",
272 | self.tr("Box Prompt"),
273 | enabled=True,
274 | )
275 | cleanPrompt = action(
276 | self.tr("Reject"),
277 | lambda: self.cleanPrompt(),
278 | 'r',
279 | "objects",
280 | self.tr("Reject"),
281 | enabled=True,
282 | )
283 |
284 | self.switchClass = action(
285 | self.tr("Class On/Off"),
286 | lambda: self.clickSwitchClass(),
287 | 'none',
288 | "objects",
289 | self.tr("Class On/Off"),
290 | enabled=True,
291 | )
292 |
293 | editMode = action(
294 | self.tr("Edit Polygons"),
295 | self.setEditMode,
296 | 'None',
297 | "edit",
298 | self.tr("Move and edit the selected polygons"),
299 | enabled=False,
300 | )
301 | saveAs = action(
302 | self.tr("&Save As"),
303 | self.saveFileAs,
304 | 'ALT+s',
305 | "save-as",
306 | self.tr("Save labels to a different file"),
307 | enabled=True,
308 | )
309 |
310 | undoLastPoint = action(
311 | self.tr("Undo last point"),
312 | self.canvas.undoLastPoint,
313 | 'U',
314 | "undo",
315 | self.tr("Undo last drawn point"),
316 | enabled=False,
317 | )
318 |
319 | hideAll = action(
320 | self.tr("&Hide\nPolygons"),
321 | functools.partial(self.togglePolygons, False),
322 | icon="eye",
323 | tip=self.tr("Hide all polygons"),
324 | enabled=False,
325 | )
326 | showAll = action(
327 | self.tr("&Show\nPolygons"),
328 | functools.partial(self.togglePolygons, True),
329 | icon="eye",
330 | tip=self.tr("Show all polygons"),
331 | enabled=False,
332 | )
333 |
334 | undo = action(
335 | self.tr("Undo"),
336 | self.undoShapeEdit,
337 | 'Ctrl+U',
338 | "undo",
339 | self.tr("Undo last add and edit of shape"),
340 | enabled=False,
341 | )
342 |
343 | save = action(
344 | self.tr("&Save"),
345 | self.saveFile,
346 | 'S',
347 | "save",
348 | self.tr("Save labels to file"),
349 | enabled=False,
350 | )
351 |
352 | delete = action(
353 | self.tr("Delete Polygons"),
354 | self.deleteSelectedShape,
355 | 'd',
356 | "cancel",
357 | self.tr("Delete the selected polygons"),
358 | enabled=False,
359 | )
360 | duplicate = action(
361 | self.tr("Duplicate Polygons"),
362 | self.duplicateSelectedShape,
363 | 'None',
364 | "copy",
365 | self.tr("Create a duplicate of the selected polygons"),
366 | enabled=False,
367 | )
368 | reduce_point = action(
369 | self.tr("Reduce Points"),
370 | self.reducePoint,
371 | 'None',
372 | "copy",
373 | self.tr("Reduce Points"),
374 | enabled=True,
375 | )
376 | edit = action(
377 | self.tr("&Edit Label"),
378 | self.editLabel,
379 | 'None',
380 | "edit",
381 | self.tr("Modify the label of the selected polygon"),
382 | enabled=False,
383 | )
384 |
385 |
386 | self.actions = utils.struct(
387 | categoryFile=categoryFile,
388 | imageDirectory=imageDirectory,
389 | saveDirectory=saveDirectory,
390 | switchClass=self.switchClass,
391 | loadSAM=LoadSAM,
392 | #autoSeg=AutoSeg,
393 | promptSeg=promptSeg,
394 | cleanPrompt=cleanPrompt,
395 | createMode=createMode,
396 | createPointMode=createPointMode,
397 | createRectangleMode=createRectangleMode,
398 | editMode=editMode,
399 | undoLastPoint=undoLastPoint,
400 | undo=undo,
401 | delete=delete,
402 | edit=edit,
403 | duplicate=duplicate,
404 | reduce_point=reduce_point,
405 | save=save,
406 | onShapesPresent=(saveAs, hideAll, showAll),
407 | menu=(
408 | createMode,
409 | editMode,
410 | undoLastPoint,
411 | undo,
412 | save,
413 | )
414 | )
415 |
416 | # Custom context menu for the canvas widget:
417 | utils.addActions(self.canvas.menus[0], self.actions.menu)
418 | utils.addActions(
419 | self.canvas.menus[1],
420 | (
421 | action("&Copy here", self.copyShape),
422 | action("&Move here", self.moveShape),
423 | ),
424 | )
425 |
426 | self.toolbar = self.addToolBar('Tool')
427 | self.toolbar.addAction(categoryFile)
428 | self.toolbar.addAction(imageDirectory)
429 | self.toolbar.addAction(saveDirectory)
430 | self.toolbar.addAction(self.switchClass)
431 | self.toolbar.addAction(LoadSAM)
432 | #self.toolbar.addAction(AutoSeg)
433 | self.toolbar.addAction(promptSeg)
434 | self.toolbar.addAction(cleanPrompt)
435 | self.toolbar.addAction(createMode)
436 | self.toolbar.addAction(createPointMode)
437 | self.toolbar.addAction(createRectangleMode)
438 | self.toolbar.addAction(editMode)
439 | self.toolbar.addAction(undoLastPoint)
440 | self.toolbar.addAction(undo)
441 | self.toolbar.addAction(delete)
442 | self.toolbar.addAction(edit)
443 | self.toolbar.addAction(duplicate)
444 | self.toolbar.addAction(reduce_point)
445 | self.toolbar.addAction(save)
446 | self.toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
447 |
448 | zoom = QtWidgets.QWidgetAction(self)
449 | zoom.setDefaultWidget(self.zoomWidget)
450 | self.zoomWidget.setWhatsThis(
451 | str(
452 | self.tr(
453 | "Zoom in or out of the image. Also accessible with "
454 | "{} from the canvas."
455 | )
456 | ).format(
457 | #utils.fmtShortcut(
458 | # "{},{}".format(shortcuts["zoom_in"], shortcuts["zoom_out"])
459 | #),
460 | utils.fmtShortcut(self.tr("Ctrl+Wheel")),
461 | )
462 | )
463 | self.zoomWidget.setEnabled(True)
464 |
465 | self.zoomWidget.valueChanged.connect(self.paintCanvas)
466 | self.canvas.actions = self.actions
467 |
468 |
469 | def saveFileAs(self, _value=False):
470 | assert not self.image.isNull(), "cannot save empty image"
471 | self._saveFile(self.saveFileDialog())
472 |
473 | def saveFile(self, _value=False):
474 | # assert not self.image.isNull(), "cannot save empty image"
475 | # if self.labelFile:
476 | # # DL20180323 - overwrite when in directory
477 | # self._saveFile(self.labelFile.filename)
478 | # elif self.output_file:
479 | # self._saveFile(self.output_file)
480 | # self.close()
481 | # else:
482 | # self._saveFile(self.saveFileDialog())
483 | #self._saveFile(self.saveFileDialog())
484 | #print(self.current_output_filename)
485 | self._saveFile(self.current_output_filename)
486 |
487 | def _saveFile(self, filename):
488 | if filename and self.saveLabels(filename):
489 | self.setClean()
490 |
491 | def saveLabels(self, filename):
492 | lf = LabelFile()
493 |
494 | def format_shape(s):
495 | data = s.other_data.copy()
496 | data.update(
497 | dict(
498 | label=s.label.encode("utf-8") if PY2 else s.label,
499 | points=[(p.x(), p.y()) for p in s.points],
500 | group_id=s.group_id,
501 | shape_type=s.shape_type,
502 | flags=s.flags,
503 | )
504 | )
505 | return data
506 |
507 | shapes = [format_shape(item.shape()) for item in self.labelList]
508 | with open(filename, 'w') as f:
509 | json.dump(shapes, f)
510 | return True
511 |
512 | def setClean(self):
513 | self.dirty = False
514 | self.actions.save.setEnabled(False)
515 | self.actions.createMode.setEnabled(True)
516 |
517 | def saveFileDialog(self):
518 | caption = self.tr("Choose File")
519 | filters = self.tr("Label files")
520 | if self.output_dir:
521 | dlg = QtWidgets.QFileDialog(
522 | self, caption, self.output_dir, filters
523 | )
524 | else:
525 | dlg = QtWidgets.QFileDialog(
526 | self, caption, self.currentPath(), filters
527 | )
528 | dlg.setDefaultSuffix(LabelFile.suffix[1:])
529 | dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
530 | dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False)
531 | dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False)
532 | basename = os.path.basename(self.current_img)[:-4]
533 | if self.output_dir:
534 | default_labelfile_name = osp.join(
535 | self.output_dir, basename + LabelFile.suffix
536 | )
537 | else:
538 | default_labelfile_name = osp.join(
539 | self.currentPath(), basename + LabelFile.suffix
540 | )
541 | filename = dlg.getSaveFileName(
542 | self,
543 | self.tr("Choose File"),
544 | default_labelfile_name,
545 | self.tr("Label files (*%s)") % LabelFile.suffix,
546 | )
547 | if isinstance(filename, tuple):
548 | filename, _ = filename
549 | return filename
550 |
551 | def currentPath(self):
552 | #return osp.dirname(str(self.filename)) if self.filename else "."
553 | return "."
554 |
555 | def loadAnno(self, filename):
556 | with open(filename,'r') as f:
557 | data = json.load(f)
558 | for shape in data:
559 | label = shape["label"]
560 | try:
561 | ttt = int(label)
562 | label = self.category_list[ttt]
563 | except:
564 | pass
565 |
566 | points = shape["points"]
567 | shape_type = shape["shape_type"]
568 | flags = shape["flags"]
569 | group_id = shape["group_id"]
570 | if not points:
571 | # skip point-empty shape
572 | continue
573 | shape = Shape(
574 | label=label,
575 | shape_type=shape_type,
576 | group_id=group_id,
577 | flags=flags
578 | )
579 | for x, y in points:
580 | shape.addPoint(QtCore.QPointF(x, y))
581 | shape.close()
582 | self.addLabel(shape)
583 | self.canvas.loadShapes([item.shape() for item in self.labelList])
584 |
585 | def clickButtonNext(self):
586 | if self.current_img_index < self.img_len - 1:
587 | self.current_img_index += 1
588 | self.current_img = self.img_list[self.current_img_index]
589 | self.loadImg()
590 |
591 | def clickButtonLast(self):
592 | if self.current_img_index > 0:
593 | self.current_img_index -= 1
594 | self.current_img = self.img_list[self.current_img_index]
595 | self.loadImg()
596 |
597 |
598 | def choose_proposal1(self):
599 | if len(self.sam_mask_proposal) > 0:
600 | self.sam_mask = self.sam_mask_proposal[0]
601 | self.canvas.setHiding()
602 | self.canvas.update()
603 |
604 | def choose_proposal2(self):
605 | if len(self.sam_mask_proposal) > 1:
606 | self.sam_mask = self.sam_mask_proposal[1]
607 | self.canvas.setHiding()
608 | self.canvas.update()
609 |
610 | def choose_proposal3(self):
611 | if len(self.sam_mask_proposal) > 2:
612 | self.sam_mask = self.sam_mask_proposal[2]
613 | self.canvas.setHiding()
614 | self.canvas.update()
615 |
616 | def choose_proposal4(self):
617 | if len(self.sam_mask_proposal) > 3:
618 | self.sam_mask = self.sam_mask_proposal[3]
619 | self.canvas.setHiding()
620 | self.canvas.update()
621 |
622 | def loadImg(self):
623 | pixmap = QPixmap(self.current_img)
624 | #pixmap = pixmap.scaled(int(0.75 * global_w), int(0.7 * global_h))
625 | self.canvas.loadPixmap(pixmap)
626 | self.img_progress_bar.setValue(self.current_img_index)
627 |
628 | img_name = os.path.basename(self.current_img)[:-4]
629 | self.current_output_filename = osp.join(self.current_output_dir, img_name + '.json')
630 | self.labelList.clear()
631 | if os.path.isfile(self.current_output_filename):
632 | self.loadAnno(self.current_output_filename)
633 | self.image_encoded_flag = False
634 |
635 |
636 | def clickFileChoose(self):
637 | directory = QFileDialog.getExistingDirectory(self, 'choose target fold','.')
638 | if directory == '':
639 | return
640 | #self.img_list = glob.glob(directory + '/*.{jpg,png,JPG,PNG}')
641 | self.img_list = glob.glob(directory + '/*.jpg') + glob.glob(directory + '/*.png')
642 | self.img_list.sort()
643 | self.img_len = len(self.img_list)
644 | if self.img_len == 0:
645 | return
646 | self.current_img_index = 0
647 | self.current_img = self.img_list[self.current_img_index]
648 | self.img_progress_bar.setMinimum(0)
649 | self.img_progress_bar.setMaximum(self.img_len-1)
650 | self.loadImg()
651 |
652 | def clickSaveChoose(self):
653 | directory = QFileDialog.getExistingDirectory(self, 'choose target fold','.')
654 | if directory == '':
655 | return
656 | else:
657 | self.current_output_dir = directory
658 | os.makedirs(self.current_output_dir, exist_ok=True)
659 | self.loadImg()
660 | return directory
661 |
662 |
663 | def clickSwitchClass(self):
664 | if self.class_on_flag:
665 | self.class_on_flag = False
666 | self.class_on_text.setText('Class Off')
667 | else:
668 | self.class_on_flag = True
669 | self.class_on_text.setText('Class On')
670 |
671 |
672 | def clickCategoryChoose(self):
673 | filename, _ = QFileDialog.getOpenFileName(self, 'choose target file','.')
674 | try:
675 | with open(filename, 'r') as f:
676 | data = f.readlines()
677 | self.category_list = [i.strip() for i in data]
678 | self.category_list.sort()
679 | self.labelDialog = LabelDialog(
680 | parent=self,
681 | labels=self.category_list,
682 | sort_labels=False,
683 | show_text_field=True,
684 | completion='contains',
685 | fit_to_content={'column': True, 'row': False},
686 | )
687 | except Exception as e:
688 | pass
689 |
690 | def clickLoadSAM(self):
691 | download_model(self.model_type)
692 | self.sam = sam_model_registry[self.model_type](checkpoint='{}.pth'.format(self.model_type))
693 | self.device = "cuda" if torch.cuda.is_available() else "cpu"
694 | self.sam.to(device=self.device)
695 | self.predictor = SamPredictor(self.sam)
696 | self.actions.loadSAM.setEnabled(False)
697 | #self.actions.autoSeg.setEnabled(True)
698 | self.actions.promptSeg.setEnabled(True)
699 |
700 | def clickAutoSeg(self):
701 | pass
702 |
703 | def getMaxId(self):
704 | max_id = -1
705 | for label in self.labelList:
706 | if label.shape().group_id != None:
707 | max_id = max(max_id, int(label.shape().group_id))
708 | return max_id
709 |
710 | def show_proposals(self, masks=None, flag=1):
711 | if flag != 1:
712 | img = cv2.imread(self.current_img)
713 | if len(img.shape) == 2:
714 | img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
715 | img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
716 | for msk_idx in range(masks.shape[0]):
717 | tmp_mask = masks[msk_idx]
718 | tmp_vis = img.copy()
719 | tmp_vis[tmp_mask > 0] = 0.5 * tmp_vis[tmp_mask > 0] + 0.5 * np.array([30,30,220])
720 | tmp_vis = cv2.resize(tmp_vis,(int(0.17 * global_w),int(0.14 * global_h)))
721 | tmp_vis = tmp_vis.astype(np.uint8)
722 | pixmap = QPixmap.fromImage(QImage(tmp_vis, tmp_vis.shape[1], tmp_vis.shape[0], tmp_vis.shape[1] * 3 , QImage.Format_RGB888))
723 | #self.button_proposal_list[msk_idx].setPixmap(pixmap)
724 | self.button_proposal_list[msk_idx].setIcon(QIcon(pixmap))
725 | self.button_proposal_list[msk_idx].setIconSize(QSize(tmp_vis.shape[1], tmp_vis.shape[0]))
726 | self.button_proposal_list[msk_idx].setShortcut(str(msk_idx+1))
727 | else:
728 | for idx, button_proposal in enumerate(self.button_proposal_list):
729 | button_proposal.setText('proprosal{}'.format(idx))
730 | button_proposal.setIconSize(QSize(0,0))
731 | self.button_proposal_list[idx].setShortcut(str(idx+1))
732 |
733 | def transform_input(self, image, box=None, points=None):
734 | if self.keep_input_size == True:
735 | return image, box, points
736 | else:
737 | h,w = image.shape[:2]
738 | scale_ratio = self.max_size / max(h,w)
739 | image = cv2.resize(image, (int(w*scale_ratio), int(h*scale_ratio)))
740 | if box is not None:
741 | box = box * scale_ratio
742 | if points is not None:
743 | points = points * scale_ratio
744 | return image, box, points
745 |
746 | def transform_output(self, masks, size):
747 | if self.keep_input_size == True:
748 | return masks
749 | else:
750 | h,w = size
751 | N = masks.shape[0]
752 | new_masks = np.zeros((N,h,w), dtype=np.uint8)
753 | for idx in range(N):
754 | new_masks[idx] = cv2.resize(masks[idx], (w,h))
755 | return new_masks
756 |
757 | def clickManualSegBBox(self):
758 | Box = self.canvas.currentBox
759 | if self.predictor is None or self.current_img == '' or Box == None:
760 | return
761 | img = cv2.imread(self.current_img)[:,:,::-1]
762 | rh, rw = img.shape[:2]
763 | input_box = np.array([Box[0].x(), Box[0].y(), Box[1].x(), Box[1].y()])
764 | img, input_box, _ = self.transform_input(img, box=input_box)
765 | if self.image_encoded_flag == False:
766 | self.predictor.set_image(img)
767 | self.image_encoded_flag = True
768 | masks, iou_prediction, _ = self.predictor.predict(
769 | point_coords=None,
770 | point_labels=None,
771 | box=input_box[None, :],
772 | multimask_output=True,
773 | )
774 | masks = self.transform_output(masks.astype(np.uint8), (rh,rw))
775 |
776 | target_idx = np.argmax(iou_prediction)
777 | self.show_proposals(masks, 0)
778 | self.sam_mask_proposal = []
779 | for msk_idx in range(masks.shape[0]):
780 | mask = masks[msk_idx].astype(np.uint8)
781 |
782 | points_list = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
783 | shape_type = 'polygon'
784 | tmp_sam_mask = []
785 | for points in points_list:
786 | area = cv2.contourArea(points)
787 | if area < 100 and len(points_list) > 1:
788 | continue
789 | pointsx = points[:,0,0]
790 | pointsy = points[:,0,1]
791 |
792 | shape = Shape(
793 | label='Object',
794 | shape_type=shape_type,
795 | group_id=self.getMaxId() + 1,
796 | )
797 | for point_index in range(pointsx.shape[0]):
798 | shape.addPoint(QtCore.QPointF(pointsx[point_index], pointsy[point_index]))
799 | shape.close()
800 | #self.addLabel(shape)
801 | tmp_sam_mask.append(shape)
802 | if msk_idx == target_idx:
803 | self.sam_mask = tmp_sam_mask
804 | self.sam_mask_proposal.append(tmp_sam_mask)
805 |
806 |
807 | def clickManualSegBox(self):
808 | ClickPos = self.canvas.currentPos
809 | ClickNeg = self.canvas.currentNeg
810 | if self.predictor is None or self.current_img == '' or (ClickPos == None and ClickNeg == None):
811 | return
812 | img = cv2.imread(self.current_img)[:,:,::-1]
813 | rh, rw = img.shape[:2]
814 |
815 | input_clicks = []
816 | input_types = []
817 | if ClickPos != None:
818 | for pos in ClickPos:
819 | input_clicks.append([int(pos.x()), int(pos.y())])
820 | input_types.append(1)
821 |
822 | if ClickNeg != None:
823 | for neg in ClickNeg:
824 | input_clicks.append([int(neg.x()), int(neg.y())])
825 | input_types.append(0)
826 | if len(input_clicks) == 0:
827 | input_clicks = None
828 | input_types = None
829 | else:
830 | input_clicks = np.array(input_clicks)
831 | input_types = np.array(input_types)
832 |
833 | img, _, input_clicks = self.transform_input(img, points=input_clicks)
834 |
835 | if self.image_encoded_flag == False:
836 | self.predictor.set_image(img)
837 | self.image_encoded_flag = True
838 | masks, iou_prediction, _ = self.predictor.predict(
839 | point_coords=input_clicks,
840 | point_labels=input_types,
841 | multimask_output=True,
842 | )
843 | masks = self.transform_output(masks.astype(np.uint8), (rh,rw))
844 |
845 | target_idx = np.argmax(iou_prediction)
846 | self.show_proposals(masks,0)
847 | self.sam_mask_proposal = []
848 |
849 | for msk_idx in range(masks.shape[0]):
850 | mask = masks[msk_idx].astype(np.uint8)
851 |
852 | points_list = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
853 | shape_type = 'polygon'
854 | tmp_sam_mask = []
855 | for points in points_list:
856 | area = cv2.contourArea(points)
857 | if area < 100 and len(points_list) > 1:
858 | continue
859 | pointsx = points[:,0,0]
860 | pointsy = points[:,0,1]
861 |
862 | shape = Shape(
863 | label='Object',
864 | shape_type=shape_type,
865 | group_id=self.getMaxId() + 1,
866 | )
867 | for point_index in range(pointsx.shape[0]):
868 | shape.addPoint(QtCore.QPointF(pointsx[point_index], pointsy[point_index]))
869 | shape.close()
870 | #self.addLabel(shape)
871 | tmp_sam_mask.append(shape)
872 | if msk_idx == target_idx:
873 | self.sam_mask = tmp_sam_mask
874 | self.sam_mask_proposal.append(tmp_sam_mask)
875 |
876 | def addSamMask(self):
877 | if len(self.sam_mask) > 0:
878 | label = 'Object'
879 | group_id = self.getMaxId() + 1
880 | if self.class_on_flag:
881 | xx = self.labelDialog.popUp(
882 | text=label,
883 | flags={},
884 | group_id=group_id,
885 | )
886 | if len(xx) == 4:
887 | label, _, group_id,_ = xx
888 | else:
889 | label, _, group_id = xx
890 | if label == None:
891 | label = 'Object'
892 | if type(group_id) != int:
893 | group_id=self.getMaxId() + 1
894 | for sam_mask in self.sam_mask:
895 | sam_mask.label = label
896 | sam_mask.group_id = group_id
897 | self.addLabel(sam_mask)
898 | self.canvas.currentBox = None
899 | self.canvas.currentPos = None
900 | self.canvas.currentNeg = None
901 | self.sam_mask = []
902 | self.sam_mask_proposal = []
903 | self.show_proposals()
904 | self.canvas.loadShapes([item.shape() for item in self.labelList])
905 | self.actions.save.setEnabled(True)
906 | self.actions.editMode.setEnabled(True)
907 |
908 |
909 |
910 | def cleanPrompt(self):
911 | self.canvas.currentBox = None
912 | self.canvas.currentPos = None
913 | self.canvas.currentNeg = None
914 | self.canvas.current = None
915 | self.sam_mask = []
916 | self.sam_mask_proposal = []
917 | self.show_proposals()
918 | self.canvas.setHiding()
919 | self.canvas.update()
920 | self.actions.editMode.setEnabled(True)
921 |
922 |
923 |
924 | def zoomRequest(self, delta, pos):
925 | canvas_width_old = self.canvas.width()
926 | units = 1.1
927 | if delta < 0:
928 | units = 0.9
929 | self.addZoom(units)
930 |
931 | canvas_width_new = self.canvas.width()
932 | if canvas_width_old != canvas_width_new:
933 | canvas_scale_factor = canvas_width_new / canvas_width_old
934 |
935 | x_shift = round(pos.x() * canvas_scale_factor) - pos.x()
936 | y_shift = round(pos.y() * canvas_scale_factor) - pos.y()
937 |
938 | self.setScroll(
939 | Qt.Horizontal,
940 | self.scrollBars[Qt.Horizontal].value() + x_shift,
941 | )
942 | self.setScroll(
943 | Qt.Vertical,
944 | self.scrollBars[Qt.Vertical].value() + y_shift,
945 | )
946 |
947 | def scrollRequest(self, delta, orientation):
948 | units = -delta * 0.1 # natural scroll
949 | bar = self.scrollBars[orientation]
950 | value = bar.value() + bar.singleStep() * units
951 | self.setScroll(orientation, value)
952 |
953 | def newShape(self):
954 | """Pop-up and give focus to the label editor.
955 |
956 | position MUST be in global coordinates.
957 | """
958 | items = self.uniqLabelList.selectedItems()
959 | text = None
960 | if items:
961 | text = items[0].data(Qt.UserRole)
962 | flags = {}
963 | group_id = None
964 | if not text:
965 | previous_text = self.labelDialog.edit.text()
966 | xx = self.labelDialog.popUp(text)
967 | if len(xx) == 4:
968 | text, flags, group_id, _ = xx
969 | else:
970 | text, flags, group_id = xx
971 | if not text:
972 | self.labelDialog.edit.setText(previous_text)
973 |
974 | if text and not self.validateLabel(text):
975 | self.errorMessage(
976 | self.tr("Invalid label"),
977 | self.tr("Invalid label '{}' with validation type '{}'").format(
978 | text, self._config["validate_label"]
979 | ),
980 | )
981 | text = ""
982 | if text:
983 | self.labelList.clearSelection()
984 | shape = self.canvas.setLastLabel(text, flags)
985 | shape.group_id = group_id
986 | self.addLabel(shape)
987 | self.actions.editMode.setEnabled(True)
988 | self.actions.undoLastPoint.setEnabled(False)
989 | self.actions.undo.setEnabled(True)
990 | self.setDirty()
991 | else:
992 | self.canvas.undoLastLine()
993 | self.canvas.shapesBackups.pop()
994 |
995 | def setDirty(self):
996 | # Even if we autosave the file, we keep the ability to undo
997 | self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
998 |
999 | # if self._config["auto_save"] or self.actions.saveAuto.isChecked():
1000 | # label_file = osp.splitext(self.imagePath)[0] + ".json"
1001 | # if self.output_dir:
1002 | # label_file_without_path = osp.basename(label_file)
1003 | # label_file = osp.join(self.output_dir, label_file_without_path)
1004 | # self.saveLabels(label_file)
1005 | # return
1006 | # self.dirty = True
1007 | self.actions.save.setEnabled(True)
1008 | # title = __appname__
1009 | # if self.filename is not None:
1010 | # title = "{} - {}*".format(title, self.filename)
1011 | # self.setWindowTitle(title)
1012 |
1013 | # React to canvas signals.
1014 | def shapeSelectionChanged(self, selected_shapes):
1015 | self._noSelectionSlot = True
1016 | for shape in self.canvas.selectedShapes:
1017 | shape.selected = False
1018 | self.labelList.clearSelection()
1019 | self.canvas.selectedShapes = selected_shapes
1020 | for shape in self.canvas.selectedShapes:
1021 | shape.selected = True
1022 | item = self.labelList.findItemByShape(shape)
1023 | self.labelList.selectItem(item)
1024 | self.labelList.scrollToItem(item)
1025 | self._noSelectionSlot = False
1026 | n_selected = len(selected_shapes)
1027 | self.actions.delete.setEnabled(n_selected)
1028 | self.actions.duplicate.setEnabled(n_selected)
1029 | self.actions.edit.setEnabled(n_selected == 1)
1030 |
1031 | def toggleDrawingSensitive(self, drawing=True):
1032 | """Toggle drawing sensitive.
1033 |
1034 | In the middle of drawing, toggling between modes should be disabled.
1035 | """
1036 | self.actions.editMode.setEnabled(not drawing)
1037 | # self.actions.undoLastPoint.setEnabled(drawing)
1038 | # self.actions.undo.setEnabled(not drawing)
1039 | # self.actions.delete.setEnabled(not drawing)
1040 | def setScroll(self, orientation, value):
1041 | self.scrollBars[orientation].setValue(int(value))
1042 | self.scroll_values[orientation][self.current_img] = value
1043 |
1044 | def toolbar(self, title, actions=None):
1045 | toolbar = self.addToolBar("%sToolBar" % title)
1046 | # toolbar.setOrientation(Qt.Vertical)
1047 | if actions:
1048 | utils.addActions(toolbar, actions)
1049 | return toolbar
1050 |
1051 | def setEditMode(self):
1052 | self.toggleDrawMode(True)
1053 |
1054 | def toggleDrawMode(self, edit=True, createMode="polygon"):
1055 | self.canvas.setEditing(edit)
1056 | self.canvas.createMode = createMode
1057 | if edit:
1058 | self.actions.createMode.setEnabled(True)
1059 | self.actions.createPointMode.setEnabled(True)
1060 | self.actions.createRectangleMode.setEnabled(True)
1061 |
1062 | else:
1063 | if createMode == "polygon":
1064 | self.actions.createPointMode.setEnabled(True)
1065 | self.actions.createMode.setEnabled(False)
1066 | self.actions.createRectangleMode.setEnabled(True)
1067 |
1068 | elif createMode == "point":
1069 | self.actions.createMode.setEnabled(True)
1070 | self.actions.createPointMode.setEnabled(False)
1071 | self.actions.createRectangleMode.setEnabled(True)
1072 | elif createMode == "rectangle":
1073 | self.actions.createMode.setEnabled(True)
1074 | self.actions.createPointMode.setEnabled(True)
1075 | self.actions.createRectangleMode.setEnabled(False)
1076 | else:
1077 | raise ValueError("Unsupported createMode: %s" % createMode)
1078 | self.actions.editMode.setEnabled(not edit)
1079 |
1080 | def validateLabel(self, label):
1081 | return True
1082 |
1083 | def labelSelectionChanged(self):
1084 | if self._noSelectionSlot:
1085 | return
1086 | if self.canvas.editing():
1087 | selected_shapes = []
1088 | for item in self.labelList.selectedItems():
1089 | selected_shapes.append(item.shape())
1090 | if selected_shapes:
1091 | self.canvas.selectShapes(selected_shapes)
1092 | else:
1093 | self.canvas.deSelectShape()
1094 |
1095 | def iou(self, target_mask, mask_list):
1096 | target_mask = target_mask.reshape(1,-1)
1097 | mask_list = mask_list.reshape(mask_list.shape[0], -1)
1098 | i = (target_mask * mask_list)
1099 | u = target_mask + mask_list - i
1100 | return i.sum(1)/u.sum(1)
1101 |
1102 |
1103 | def polygon2mask(self,polygon, size):
1104 | mask = np.zeros((size)) # h,w
1105 | contours = np.array(polygon)
1106 | mask = cv2.fillPoly(mask, [contours.astype(np.int32)],1)
1107 | return mask.astype(np.uint8)
1108 |
1109 | def mask2polygon(self, mask):
1110 | contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
1111 | contours = np.array(contours[0])
1112 | return contours
1113 |
1114 | def editLabel(self, item=None):
1115 | if item and not isinstance(item, LabelListWidgetItem):
1116 | raise TypeError("item must be LabelListWidgetItem type")
1117 |
1118 | if not self.canvas.editing():
1119 | return
1120 | if not item:
1121 | item = self.currentItem()
1122 | if item is None:
1123 | return
1124 | shape = item.shape()
1125 | if shape is None:
1126 | return
1127 | xx = self.labelDialog.popUp(
1128 | text=shape.label,
1129 | flags=shape.flags,
1130 | group_id=shape.group_id,
1131 | )
1132 | if len(xx) == 4:
1133 | text, flags, group_id,_ = xx
1134 | else:
1135 | text, flags, group_id = xx
1136 | if text is None:
1137 | return
1138 | if not self.validateLabel(text):
1139 | self.errorMessage(
1140 | self.tr("Invalid label"),
1141 | self.tr("Invalid label '{}' with validation type '{}'").format(
1142 | text, self._config["validate_label"]
1143 | ),
1144 | )
1145 | return
1146 | shape.label = text
1147 | shape.flags = flags
1148 | shape.group_id = group_id
1149 |
1150 | self._update_shape_color(shape)
1151 | if shape.group_id is None:
1152 | item.setText(
1153 | '{} ●'.format(
1154 | html.escape(shape.label), *shape.fill_color.getRgb()[:3]
1155 | )
1156 | )
1157 | else:
1158 | item.setText("({}) {}".format(shape.group_id, shape.label))
1159 | self.setDirty()
1160 | if self.uniqLabelList.findItemByLabel(shape.label) is None:
1161 | item = self.uniqLabelList.createItemFromLabel(shape.label)
1162 | self.uniqLabelList.addItem(item)
1163 | # rgb = self._get_rgb_by_label(shape.label)
1164 | rgb = self._get_rgb_by_label(shape.group_id)
1165 | self.uniqLabelList.setItemLabel(item, shape.label, rgb)
1166 |
1167 | def labelItemChanged(self, item):
1168 | shape = item.shape()
1169 | self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked)
1170 |
1171 | def labelOrderChanged(self):
1172 | self.setDirty()
1173 | self.canvas.loadShapes([item.shape() for item in self.labelList])
1174 |
1175 | def addLabel(self, shape):
1176 | if shape.group_id is None:
1177 | text = shape.label
1178 | else:
1179 | text = "({}) {}".format(shape.group_id, shape.label)
1180 | label_list_item = LabelListWidgetItem(text, shape)
1181 | self.labelList.addItem(label_list_item)
1182 | if self.uniqLabelList.findItemByLabel(shape.label) is None:
1183 | item = self.uniqLabelList.createItemFromLabel(shape.label)
1184 | self.uniqLabelList.addItem(item)
1185 | # rgb = self._get_rgb_by_label(shape.label)
1186 | rgb = self._get_rgb_by_label(shape.group_id)
1187 | self.uniqLabelList.setItemLabel(item, shape.label, rgb)
1188 | self.labelDialog.addLabelHistory(shape.label)
1189 | for action in self.actions.onShapesPresent:
1190 | action.setEnabled(True)
1191 |
1192 | self._update_shape_color(shape)
1193 | label_list_item.setText(
1194 | '{} ●'.format(
1195 | html.escape(text), *shape.fill_color.getRgb()[:3]
1196 | )
1197 | )
1198 | def _get_rgb_by_label(self, label):
1199 | label = str(label)
1200 | item = self.uniqLabelList.findItemByLabel(label)
1201 | if item is None:
1202 | item = self.uniqLabelList.createItemFromLabel(label)
1203 | self.uniqLabelList.addItem(item)
1204 | rgb = self._get_rgb_by_label(label)
1205 | self.uniqLabelList.setItemLabel(item, label, rgb)
1206 | label_id = self.uniqLabelList.indexFromItem(item).row() + 1
1207 | label_id += 0
1208 | return LABEL_COLORMAP[label_id % len(LABEL_COLORMAP)]
1209 |
1210 | def togglePolygons(self, value):
1211 | for item in self.labelList:
1212 | item.setCheckState(Qt.Checked if value else Qt.Unchecked)
1213 |
1214 | def _update_shape_color(self, shape):
1215 | # r, g, b = self._get_rgb_by_label(shape.label)
1216 | r, g, b = self._get_rgb_by_label(shape.group_id)
1217 | shape.line_color = QtGui.QColor(r, g, b)
1218 | shape.vertex_fill_color = QtGui.QColor(r, g, b)
1219 | shape.hvertex_fill_color = QtGui.QColor(255, 255, 255)
1220 | shape.fill_color = QtGui.QColor(r, g, b, 128)
1221 | shape.select_line_color = QtGui.QColor(255, 255, 255)
1222 | shape.select_fill_color = QtGui.QColor(r, g, b, 155)
1223 |
1224 | def undoShapeEdit(self):
1225 | self.canvas.restoreShape()
1226 | self.labelList.clear()
1227 | self.loadShapes(self.canvas.shapes)
1228 | self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
1229 |
1230 | def loadShapes(self, shapes, replace=True):
1231 | self._noSelectionSlot = True
1232 | for shape in shapes:
1233 | self.addLabel(shape)
1234 | self.labelList.clearSelection()
1235 | self._noSelectionSlot = False
1236 | self.canvas.loadShapes(shapes, replace=replace)
1237 |
1238 |
1239 | def moveShape(self):
1240 | self.canvas.endMove(copy=False)
1241 | self.setDirty()
1242 |
1243 | def copyShape(self):
1244 | self.canvas.endMove(copy=True)
1245 | for shape in self.canvas.selectedShapes:
1246 | self.addLabel(shape)
1247 | self.labelList.clearSelection()
1248 | self.setDirty()
1249 | def deleteSelectedShape(self):
1250 | #yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
1251 | #msg = self.tr(
1252 | # "You are about to permanently delete {} polygons, "
1253 | # "proceed anyway?"
1254 | #).format(len(self.canvas.selectedShapes))
1255 | #if yes == QtWidgets.QMessageBox.warning(
1256 | # self, self.tr("Attention"), msg, yes | no, yes
1257 | #):
1258 | self.remLabels(self.canvas.deleteSelected())
1259 | self.setDirty()
1260 | if self.noShapes():
1261 | for action in self.actions.onShapesPresent:
1262 | action.setEnabled(False)
1263 | def duplicateSelectedShape(self):
1264 | added_shapes = self.canvas.duplicateSelectedShapes()
1265 | self.labelList.clearSelection()
1266 | for shape in added_shapes:
1267 | self.addLabel(shape)
1268 | self.setDirty()
1269 |
1270 | def reducePoint(self):
1271 | def format_shape(s):
1272 | data = s.other_data.copy()
1273 | data.update(
1274 | dict(
1275 | label=s.label.encode("utf-8") if PY2 else s.label,
1276 | points=[(p.x(), p.y()) for p in s.points],
1277 | group_id=s.group_id,
1278 | shape_type=s.shape_type,
1279 | flags=s.flags,
1280 | )
1281 | )
1282 | return data
1283 | shapes = self.current_img
1284 | shapes = [format_shape(item.shape()) for item in self.labelList.selectedItems()]
1285 | rm_shapes = [item.shape() for item in self.labelList.selectedItems()]
1286 | self.remLabels(rm_shapes)
1287 | for shape in shapes:
1288 | points = shape['points']
1289 | min_dis = self.get_min_dis(points)
1290 | points_new = [points[0]]
1291 | for i in range(1,len(points)):
1292 | d = math.sqrt((points[i][0] - points_new[-1][0]) ** 2 + (points[i][1] - points_new[-1][1]) ** 2)
1293 | if d > (min_dis * 1.5):
1294 | points_new.append(points[i])
1295 | shape['points'] = points_new
1296 | #self.labelList.clear()
1297 | for tmp_shape in shapes:
1298 | shape = Shape(
1299 | label=tmp_shape['label'],
1300 | shape_type=tmp_shape['shape_type'],
1301 | group_id=tmp_shape['group_id'],
1302 | )
1303 | for point_index in range(len(tmp_shape['points'])):
1304 | shape.addPoint(QtCore.QPointF(tmp_shape['points'][point_index][0], tmp_shape['points'][point_index][1]))
1305 | shape.close()
1306 | self.addLabel(shape)
1307 | tmp_item = self.labelList.findItemByShape(shape)
1308 | self.labelList.selectItem(tmp_item)
1309 | self.labelList.scrollToItem(tmp_item)
1310 | self.canvas.loadShapes([item.shape() for item in self.labelList])
1311 | self.actions.save.setEnabled(True)
1312 |
1313 | def get_min_dis(self, points):
1314 | min_dis = 10000
1315 | if len(points) >= 2:
1316 | points_new = [points[0]]
1317 | for i in range(1,len(points)):
1318 | d = math.sqrt((points[i][0] - points_new[-1][0]) ** 2 + (points[i][1] - points_new[-1][1]) ** 2)
1319 | min_dis = min(min_dis, d)
1320 | points_new.append(points[i])
1321 | return min_dis
1322 |
1323 |
1324 |
1325 | def pasteSelectedShape(self):
1326 | self.loadShapes(self._copied_shapes, replace=False)
1327 | self.setDirty()
1328 |
1329 | def copySelectedShape(self):
1330 | self._copied_shapes = [s.copy() for s in self.canvas.selectedShapes]
1331 | self.actions.paste.setEnabled(len(self._copied_shapes) > 0)
1332 |
1333 | def currentItem(self):
1334 | items = self.labelList.selectedItems()
1335 | if items:
1336 | return items[0]
1337 | return None
1338 |
1339 | def remLabels(self, shapes):
1340 | for shape in shapes:
1341 | item = self.labelList.findItemByShape(shape)
1342 | self.labelList.removeItem(item)
1343 |
1344 |
1345 | def noShapes(self):
1346 | return not len(self.labelList)
1347 |
1348 | def addZoom(self, increment=1.1):
1349 | zoom_value = self.zoomWidget.value() * increment
1350 | if increment > 1:
1351 | zoom_value = math.ceil(zoom_value)
1352 | else:
1353 | zoom_value = math.floor(zoom_value)
1354 | self.setZoom(zoom_value)
1355 |
1356 | def setZoom(self, value):
1357 | self.zoomMode = self.MANUAL_ZOOM
1358 | self.zoomWidget.setValue(value)
1359 | self.zoom_values[self.current_img] = (self.zoomMode, value)
1360 |
1361 | def paintCanvas(self):
1362 | self.canvas.scale = 0.01 * self.zoomWidget.value()
1363 | self.canvas.adjustSize()
1364 | self.canvas.update()
1365 |
1366 |
1367 | def get_parser():
1368 | parser = argparse.ArgumentParser(description="pixel annotator by GroundedSAM")
1369 | parser.add_argument(
1370 | "--app_resolution",
1371 | default='1000,1600',
1372 | )
1373 | parser.add_argument(
1374 | "--model_type",
1375 | default='vit_b',
1376 | )
1377 | parser.add_argument(
1378 | "--keep_input_size",
1379 | type=bool,
1380 | default=True,
1381 | )
1382 | parser.add_argument(
1383 | "--max_size",
1384 | default=720,
1385 | )
1386 | return parser
1387 |
1388 | if __name__ == '__main__':
1389 | parser = get_parser()
1390 | global_h, global_w = [int(i) for i in parser.parse_args().app_resolution.split(',')]
1391 | model_type = parser.parse_args().model_type
1392 | keep_input_size = parser.parse_args().keep_input_size
1393 | max_size = parser.parse_args().max_size
1394 | app = QApplication(sys.argv)
1395 | main = MainWindow(global_h=global_h, global_w=global_w, model_type=model_type, keep_input_size=keep_input_size, max_size=max_size)
1396 | main.show()
1397 | sys.exit(app.exec_())
1398 |
--------------------------------------------------------------------------------
/canvas.py:
--------------------------------------------------------------------------------
1 | from qtpy import QtCore
2 | from qtpy import QtGui
3 | from qtpy import QtWidgets
4 |
5 | from labelme import QT5
6 | from shape import Shape
7 | import labelme.utils
8 | from collections import namedtuple
9 | import cv2
10 | import numpy as np
11 | import torch
12 |
13 |
14 | # TODO(unknown):
15 | # - [maybe] Find optimal epsilon value.
16 |
17 |
18 | CURSOR_DEFAULT = QtCore.Qt.ArrowCursor
19 | CURSOR_POINT = QtCore.Qt.PointingHandCursor
20 | CURSOR_DRAW = QtCore.Qt.CrossCursor
21 | CURSOR_MOVE = QtCore.Qt.ClosedHandCursor
22 | CURSOR_GRAB = QtCore.Qt.OpenHandCursor
23 |
24 | MOVE_SPEED = 5.0
25 | Click = namedtuple('Click', ['is_positive', 'coords'])
26 | class Canvas(QtWidgets.QWidget):
27 |
28 | zoomRequest = QtCore.Signal(int, QtCore.QPoint)
29 | scrollRequest = QtCore.Signal(int, int)
30 | newShape = QtCore.Signal()
31 | selectionChanged = QtCore.Signal(list)
32 | shapeMoved = QtCore.Signal()
33 | drawingPolygon = QtCore.Signal(bool)
34 | vertexSelected = QtCore.Signal(bool)
35 |
36 |
37 | CREATE, EDIT = 0, 1
38 |
39 | # polygon, rectangle, line, or point
40 | _createMode = "polygon"
41 |
42 | _fill_drawing = False
43 |
44 | def __init__(self, *args, **kwargs):
45 | self.epsilon = kwargs.pop("epsilon", 10.0)
46 | self.double_click = kwargs.pop("double_click", "close")
47 | if self.double_click not in [None, "close"]:
48 | raise ValueError(
49 | "Unexpected value for double_click event: {}".format(
50 | self.double_click
51 | )
52 | )
53 | self.num_backups = kwargs.pop("num_backups", 10)
54 | self._crosshair = kwargs.pop(
55 | "crosshair",
56 | {
57 | "polygon": False,
58 | "rectangle": True,
59 | "circle": False,
60 | "line": False,
61 | "point": False,
62 | "linestrip": False,
63 | },
64 | )
65 | self.app = kwargs.pop("app", None)
66 | super(Canvas, self).__init__(*args, **kwargs)
67 | # Initialise local state.
68 | self.mode = self.EDIT
69 | self.shapes = []
70 | self.shapesBackups = []
71 | self.current = None
72 | self.currentPos = None
73 | self.currentNeg = None
74 | self.currentBox = None
75 | self.selectedShapes = [] # save the selected shapes here
76 | self.selectedShapesCopy = []
77 | # self.line represents:
78 | # - createMode == 'polygon': edge from last point to current
79 | # - createMode == 'rectangle': diagonal line of the rectangle
80 | # - createMode == 'line': the line
81 | # - createMode == 'point': the point
82 | self.line = Shape()
83 | self.prevPoint = QtCore.QPoint()
84 | self.prevMovePoint = QtCore.QPoint()
85 | self.offsets = QtCore.QPoint(), QtCore.QPoint()
86 | self.scale = 1
87 | self.pixmap = QtGui.QPixmap()
88 | self.visible = {}
89 | self._hideBackround = False
90 | self.hideBackround = False
91 | self.hShape = None
92 | self.prevhShape = None
93 | self.hVertex = None
94 | self.prevhVertex = None
95 | self.hEdge = None
96 | self.prevhEdge = None
97 | self.movingShape = False
98 | self.snapping = True
99 | self.hShapeIsSelected = False
100 | self._painter = QtGui.QPainter()
101 | self._cursor = CURSOR_DEFAULT
102 | # Menus:
103 | # 0: right-click without selection and dragging of shapes
104 | # 1: right-click with selection and dragging of shapes
105 | self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
106 | # Set widget options.
107 | self.setMouseTracking(True)
108 | self.setFocusPolicy(QtCore.Qt.WheelFocus)
109 |
110 |
111 |
112 | def fillDrawing(self):
113 | return self._fill_drawing
114 |
115 | def setFillDrawing(self, value):
116 | self._fill_drawing = value
117 |
118 | @property
119 | def createMode(self):
120 | return self._createMode
121 |
122 | @createMode.setter
123 | def createMode(self, value):
124 | if value not in [
125 | "polygon",
126 | "rectangle",
127 | "circle",
128 | "line",
129 | "point",
130 | "linestrip",
131 | ]:
132 | raise ValueError("Unsupported createMode: %s" % value)
133 | self._createMode = value
134 |
135 | def storeShapes(self):
136 | shapesBackup = []
137 | for shape in self.shapes:
138 | shapesBackup.append(shape.copy())
139 | if len(self.shapesBackups) > self.num_backups:
140 | self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :]
141 | self.shapesBackups.append(shapesBackup)
142 |
143 | @property
144 | def isShapeRestorable(self):
145 | # We save the state AFTER each edit (not before) so for an
146 | # edit to be undoable, we expect the CURRENT and the PREVIOUS state
147 | # to be in the undo stack.
148 | if len(self.shapesBackups) < 2:
149 | return False
150 | return True
151 |
152 | def restoreShape(self):
153 | # This does _part_ of the job of restoring shapes.
154 | # The complete process is also done in app.py::undoShapeEdit
155 | # and app.py::loadShapes and our own Canvas::loadShapes function.
156 | if not self.isShapeRestorable:
157 | return
158 | self.shapesBackups.pop() # latest
159 |
160 | # The application will eventually call Canvas.loadShapes which will
161 | # push this right back onto the stack.
162 | shapesBackup = self.shapesBackups.pop()
163 | self.shapes = shapesBackup
164 | self.selectedShapes = []
165 | for shape in self.shapes:
166 | shape.selected = False
167 | self.update()
168 |
169 | def enterEvent(self, ev):
170 | self.overrideCursor(self._cursor)
171 |
172 | def leaveEvent(self, ev):
173 | self.unHighlight()
174 | self.restoreCursor()
175 |
176 | def focusOutEvent(self, ev):
177 | self.restoreCursor()
178 |
179 | def isVisible(self, shape):
180 | return self.visible.get(shape, True)
181 |
182 | def drawing(self):
183 | return self.mode == self.CREATE
184 |
185 | def editing(self):
186 | return self.mode == self.EDIT
187 |
188 | def setEditing(self, value=True):
189 | self.mode = self.EDIT if value else self.CREATE
190 | if self.mode == self.EDIT:
191 | # CREATE -> EDIT
192 | self.repaint() # clear crosshair
193 | else:
194 | # EDIT -> CREATE
195 | self.unHighlight()
196 | self.deSelectShape()
197 |
198 | def unHighlight(self):
199 | if self.hShape:
200 | self.hShape.highlightClear()
201 | self.update()
202 | self.prevhShape = self.hShape
203 | self.prevhVertex = self.hVertex
204 | self.prevhEdge = self.hEdge
205 | self.hShape = self.hVertex = self.hEdge = None
206 |
207 | def selectedVertex(self):
208 | return self.hVertex is not None
209 |
210 | def selectedEdge(self):
211 | return self.hEdge is not None
212 |
213 | def mouseMoveEvent(self, ev):
214 | """Update line with last point and current coordinates."""
215 | try:
216 | if QT5:
217 | pos = self.transformPos(ev.localPos())
218 | else:
219 | pos = self.transformPos(ev.posF())
220 | except AttributeError:
221 | return
222 |
223 | self.prevMovePoint = pos
224 | self.restoreCursor()
225 |
226 | # Polygon drawing.
227 | if self.drawing():
228 | self.line.shape_type = self.createMode
229 |
230 | self.overrideCursor(CURSOR_DRAW)
231 | if not self.current:
232 | self.repaint() # draw crosshair
233 | return
234 |
235 | if self.outOfPixmap(pos):
236 | # Don't allow the user to draw outside the pixmap.
237 | # Project the point to the pixmap's edges.
238 | pos = self.intersectionPoint(self.current[-1], pos)
239 | elif (
240 | self.snapping
241 | and len(self.current) > 1
242 | and self.createMode == "polygon"
243 | and self.closeEnough(pos, self.current[0])
244 | ):
245 | # Attract line to starting point and
246 | # colorise to alert the user.
247 | pos = self.current[0]
248 | self.overrideCursor(CURSOR_POINT)
249 | self.current.highlightVertex(0, Shape.NEAR_VERTEX)
250 | if self.createMode in ["polygon", "linestrip"]:
251 | self.line[0] = self.current[-1]
252 | self.line[1] = pos
253 | elif self.createMode == "rectangle":
254 | self.line.points = [self.current[0], pos]
255 | self.line.close()
256 | elif self.createMode == "circle":
257 | self.line.points = [self.current[0], pos]
258 | self.line.shape_type = "circle"
259 | elif self.createMode == "line":
260 | self.line.points = [self.current[0], pos]
261 | self.line.close()
262 | elif self.createMode == "point":
263 | #self.line.points = [self.current[0]]
264 | #self.line.close()
265 | pass
266 | self.repaint()
267 | self.current.highlightClear()
268 | if QtCore.Qt.RightButton & ev.buttons():
269 | if self.current:
270 | # Add point to existing shape.
271 | if self.createMode == "polygon":
272 | d2 = (self.current[-1].x() - self.line[1].x()) ** 2 + (self.current[-1].y() - self.line[1].y()) ** 2
273 | if d2 >= 80:
274 | self.current.addPoint(self.line[1])
275 | self.line[0] = self.current[-1]
276 | if self.current.isClosed():
277 | self.finalise()
278 | elif self.createMode in ["rectangle", "circle", "line"]:
279 | assert len(self.current.points) == 1
280 | self.current.points = self.line.points
281 | self.finalise()
282 | elif self.createMode == "linestrip":
283 | self.current.addPoint(self.line[1])
284 | self.line[0] = self.current[-1]
285 | if int(ev.modifiers()) == QtCore.Qt.ControlModifier:
286 | self.finalise()
287 |
288 | elif not self.outOfPixmap(pos):
289 | # Create new shape.
290 | self.current = Shape(shape_type=self.createMode)
291 | self.current.addPoint(pos)
292 | if self.createMode == "point":
293 | self.setHiding()
294 | self.update()
295 |
296 | else:
297 | if self.createMode == "circle":
298 | self.current.shape_type = "circle"
299 | self.line.points = [pos, pos]
300 | self.setHiding()
301 | self.drawingPolygon.emit(True)
302 | self.update()
303 | return
304 | # # Polygon copy moving.
305 | # if QtCore.Qt.RightButton & ev.buttons():
306 | # if self.selectedShapesCopy and self.prevPoint:
307 | # self.overrideCursor(CURSOR_MOVE)
308 | # self.boundedMoveShapes(self.selectedShapesCopy, pos)
309 | # self.repaint()
310 | # elif self.selectedShapes:
311 | # self.selectedShapesCopy = [
312 | # s.copy() for s in self.selectedShapes
313 | # ]
314 | # self.repaint()
315 | # return
316 |
317 | # Polygon/Vertex moving.
318 | if QtCore.Qt.LeftButton & ev.buttons():
319 | if self.selectedVertex():
320 | self.boundedMoveVertex(pos)
321 | self.repaint()
322 | self.movingShape = True
323 | elif self.selectedShapes and self.prevPoint:
324 | self.overrideCursor(CURSOR_MOVE)
325 | self.boundedMoveShapes(self.selectedShapes, pos)
326 | self.repaint()
327 | self.movingShape = True
328 | return
329 | if self.editing() and QtCore.Qt.RightButton and ev.buttons():
330 | if self.current:
331 | index, shape = self.hVertex, self.hShape
332 | #print(self.selectedVertex(), index, self.modified_memory[0])
333 | if self.selectedVertex():
334 | index, shape = self.hVertex, self.hShape
335 | if index != self.modified_memory[0] and shape == self.modified_memory[1]:
336 | self.start_modify_flag = False
337 | if self.start_modify_flag == True:
338 | d2 = (self.current[-1].x() - pos.x()) ** 2 + (self.current[-1].y() - pos.y()) ** 2
339 | if d2 >= 80:
340 | self.current.addPoint(pos)
341 | self.setHiding()
342 | self.update()
343 | else:
344 | if len(self.modified_memory) == 2:
345 | self.modified_memory = self.modified_memory + [index, shape]
346 |
347 | #print(self.selectedVertex())
348 | #if self.selectedVertex():
349 | # self.boundedMoveVertex(pos)
350 | # self.repaint()
351 | # self.movingShape = True
352 |
353 | # Just hovering over the canvas, 2 possibilities:
354 | # - Highlight shapes
355 | # - Highlight vertex
356 | # Update shape/vertex fill and tooltip value accordingly.
357 | self.setToolTip(self.tr("Image"))
358 | for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
359 | # Look for a nearby vertex to highlight. If that fails,
360 | # check if we happen to be inside a shape.
361 | index = shape.nearestVertex(pos, self.epsilon / self.scale)
362 | index_edge = shape.nearestEdge(pos, self.epsilon / self.scale)
363 | if index is not None:
364 | if self.selectedVertex():
365 | self.hShape.highlightClear()
366 | self.prevhVertex = self.hVertex = index
367 | self.prevhShape = self.hShape = shape
368 | self.prevhEdge = self.hEdge
369 | self.hEdge = None
370 | shape.highlightVertex(index, shape.MOVE_VERTEX)
371 | self.overrideCursor(CURSOR_POINT)
372 | self.setToolTip(self.tr("Click & drag to move point"))
373 | self.setStatusTip(self.toolTip())
374 | self.update()
375 | break
376 | elif index_edge is not None and shape.canAddPoint():
377 | if self.selectedVertex():
378 | self.hShape.highlightClear()
379 | self.prevhVertex = self.hVertex
380 | self.hVertex = None
381 | self.prevhShape = self.hShape = shape
382 | self.prevhEdge = self.hEdge = index_edge
383 | self.overrideCursor(CURSOR_POINT)
384 | self.setToolTip(self.tr("Click to create point"))
385 | self.setStatusTip(self.toolTip())
386 | self.update()
387 | break
388 | elif shape.containsPoint(pos):
389 | if self.selectedVertex():
390 | self.hShape.highlightClear()
391 | self.prevhVertex = self.hVertex
392 | self.hVertex = None
393 | self.prevhShape = self.hShape = shape
394 | self.prevhEdge = self.hEdge
395 | self.hEdge = None
396 | self.setToolTip(
397 | self.tr("Click & drag to move shape '%s'") % shape.label
398 | )
399 | self.setStatusTip(self.toolTip())
400 | self.overrideCursor(CURSOR_GRAB)
401 | self.update()
402 | break
403 | else: # Nothing found, clear highlights, reset state.
404 | self.unHighlight()
405 | self.vertexSelected.emit(self.hVertex is not None)
406 |
407 | def addPointToEdge(self):
408 | shape = self.prevhShape
409 | index = self.prevhEdge
410 | point = self.prevMovePoint
411 | if shape is None or index is None or point is None:
412 | return
413 | shape.insertPoint(index, point)
414 | shape.highlightVertex(index, shape.MOVE_VERTEX)
415 | self.hShape = shape
416 | self.hVertex = index
417 | self.hEdge = None
418 | self.movingShape = True
419 |
420 | def removeSelectedPoint(self):
421 | shape = self.prevhShape
422 | index = self.prevhVertex
423 | if shape is None or index is None:
424 | return
425 | shape.removePoint(index)
426 | shape.highlightClear()
427 | self.hShape = shape
428 | self.prevhVertex = None
429 | self.movingShape = True # Save changes
430 |
431 |
432 | def mousePressEvent(self, ev):
433 | if QT5:
434 | pos = self.transformPos(ev.localPos())
435 | else:
436 | pos = self.transformPos(ev.posF())
437 | if ev.button() == QtCore.Qt.LeftButton:
438 | if self.drawing():
439 | if self.current:
440 | # Add point to existing shape.
441 | if self.createMode == "polygon":
442 | self.current.addPoint(self.line[1])
443 | self.line[0] = self.current[-1]
444 | if self.current.isClosed():
445 | self.finalise()
446 | elif self.createMode in ["circle", "line"]:
447 | assert len(self.current.points) == 1
448 | self.current.points = self.line.points
449 | self.finalise()
450 | elif self.createMode == "linestrip":
451 | self.current.addPoint(self.line[1])
452 | self.line[0] = self.current[-1]
453 | if int(ev.modifiers()) == QtCore.Qt.ControlModifier:
454 | self.finalise()
455 |
456 | elif not self.outOfPixmap(pos):
457 | # Create new shape.
458 | if self.createMode == "point":
459 | pass
460 | elif self.createMode == "rectangle":
461 | pass
462 | else:
463 | self.current = Shape(shape_type=self.createMode)
464 | self.current.addPoint(pos)
465 | if self.createMode == "circle":
466 | self.current.shape_type = "circle"
467 | self.line.points = [pos, pos]
468 | self.setHiding()
469 | self.drawingPolygon.emit(True)
470 | self.update()
471 |
472 | if self.currentBox:
473 | if self.createMode == "rectangle":
474 | if len(self.currentBox.points) == 1:
475 | self.currentBox.addPoint(pos)
476 | self.currentBox.close()
477 | self.app.clickManualSegBBox()
478 | self.setHiding()
479 | self.update()
480 | else:
481 | self.currentBox = Shape(shape_type=self.createMode)
482 | self.currentBox.addPoint(pos)
483 | self.line.points = [pos, pos]
484 | self.setHiding()
485 | self.drawingPolygon.emit(True)
486 | self.update()
487 | # elif self.createMode == "linestrip":
488 | # self.currentBox.addPoint(self.line[1])
489 | # self.line[0] = self.currentBox[-1]
490 | elif not self.outOfPixmap(pos):
491 | if self.createMode == "rectangle":
492 | self.currentBox = Shape(shape_type=self.createMode)
493 | self.currentBox.addPoint(pos)
494 | self.line.points = [pos, pos]
495 | self.setHiding()
496 | self.drawingPolygon.emit(True)
497 | self.update()
498 |
499 |
500 | if self.currentPos:
501 | if self.createMode == "point":
502 | self.currentPos.addPoint(pos)
503 | self.setHiding()
504 | self.update()
505 | self.app.clickManualSegBox()
506 | self.setHiding()
507 | self.update()
508 | elif not self.outOfPixmap(pos):
509 | if self.createMode == "point":
510 | self.currentPos = Shape(shape_type=self.createMode)
511 | self.currentPos.addPoint(pos)
512 | self.setHiding()
513 | self.update()
514 | self.app.clickManualSegBox()
515 | self.setHiding()
516 | self.update()
517 | elif self.editing():
518 | if self.selectedEdge():
519 | self.addPointToEdge()
520 | elif (
521 | self.selectedVertex()
522 | and int(ev.modifiers()) == QtCore.Qt.ShiftModifier
523 | ):
524 | # Delete point if: left-click + SHIFT on a point
525 | self.removeSelectedPoint()
526 |
527 | group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
528 | self.selectShapePoint(pos, multiple_selection_mode=group_mode)
529 | self.prevPoint = pos
530 | self.repaint()
531 | elif ev.button() == QtCore.Qt.RightButton:
532 | if self.createMode == "point":
533 | self.setHiding()
534 | self.update()
535 | if self.drawing():
536 | if self.current:
537 | # Add point to existing shape.
538 | if self.createMode == "polygon":
539 | self.current.addPoint(self.line[1])
540 | self.line[0] = self.current[-1]
541 | if self.current.isClosed():
542 | self.finalise()
543 | elif self.createMode in ["circle", "line"]:
544 | assert len(self.current.points) == 1
545 | self.current.points = self.line.points
546 | self.finalise()
547 | elif self.createMode == "linestrip":
548 | self.current.addPoint(self.line[1])
549 | self.line[0] = self.current[-1]
550 | if int(ev.modifiers()) == QtCore.Qt.ControlModifier:
551 | self.finalise()
552 |
553 | elif not self.outOfPixmap(pos):
554 | # Create new shape.
555 | if self.createMode == "point":
556 | pass
557 | if self.createMode == "rectangle":
558 | pass
559 | else:
560 | self.current = Shape(shape_type=self.createMode)
561 | self.current.addPoint(pos)
562 | if self.createMode == "circle":
563 | self.current.shape_type = "circle"
564 | self.line.points = [pos, pos]
565 | self.setHiding()
566 | self.drawingPolygon.emit(True)
567 | self.update()
568 |
569 | if self.currentBox:
570 | if self.createMode == "rectangle":
571 | if len(self.currentBox.points) == 1:
572 | self.currentBox.addPoint(pos)
573 | self.currentBox.close()
574 | self.app.clickManualSegBBox()
575 | self.setHiding()
576 | self.update()
577 | else:
578 | self.currentBox = Shape(shape_type=self.createMode)
579 | self.currentBox.addPoint(pos)
580 | self.line.points = [pos, pos]
581 | self.setHiding()
582 | self.drawingPolygon.emit(True)
583 | self.update()
584 | # elif self.createMode == "linestrip":
585 | # self.currentBox.addPoint(self.line[1])
586 | # self.line[0] = self.currentBox[-1]
587 | elif not self.outOfPixmap(pos):
588 | if self.createMode == "rectangle":
589 | self.currentBox = Shape(shape_type=self.createMode)
590 | self.currentBox.addPoint(pos)
591 | self.line.points = [pos, pos]
592 | self.setHiding()
593 | self.drawingPolygon.emit(True)
594 | self.update()
595 |
596 | if self.currentNeg:
597 | if self.createMode == "point":
598 | self.currentNeg.addPoint(pos)
599 | self.setHiding()
600 | self.update()
601 | self.app.clickManualSegBox()
602 | self.setHiding()
603 | self.update()
604 | elif not self.outOfPixmap(pos):
605 | if self.createMode == "point":
606 | self.currentNeg = Shape(shape_type=self.createMode)
607 | self.currentNeg.addPoint(pos)
608 | self.setHiding()
609 | self.update()
610 | self.app.clickManualSegBox()
611 | self.setHiding()
612 | self.update()
613 |
614 | if self.editing():
615 | if (self.selectedVertex()
616 | and int(ev.modifiers()) == QtCore.Qt.ShiftModifier
617 | ):
618 | # Delete point if: left-click + SHIFT on a point
619 | self.removeSelectedPoint()
620 | else:
621 | if not self.selectedVertex():
622 | self.current = None
623 | else:
624 | if not self.outOfPixmap(pos) and self.selectedVertex() and (not self.current):
625 | # Create new shape.
626 | self.current = Shape(shape_type=self.createMode)
627 | self.current.addPoint(pos)
628 | self.line.points = [pos, pos]
629 | self.start_modify_flag = True
630 | index, shape = self.hVertex, self.hShape
631 | self.modified_memory = [index, shape]
632 | self.setHiding()
633 | self.update()
634 | elif self.current and self.selectedVertex() and len(self.modified_memory) == 4:
635 |
636 | tmp_points = self.modified_memory[1].points
637 | add_points = self.current.points
638 | ind1, ind2 = self.modified_memory[0], self.modified_memory[2]
639 | index, shape = self.hVertex, self.hShape
640 | if ind1 < ind2:
641 | if index <= ind2 and index >= ind1:
642 | modified_points = tmp_points[:ind1] + add_points + tmp_points[ind2:]
643 | elif index > ind2:
644 | modified_points = add_points[::-1] + tmp_points[ind1:ind2]
645 | elif index < ind1:
646 | modified_points = add_points[::-1] + tmp_points[ind1:ind2]
647 | elif ind1 >= ind2:
648 | if index <= ind1 and index >= ind2:
649 | modified_points = tmp_points[:ind2] + add_points[::-1] + tmp_points[ind1:]
650 | elif index > ind1:
651 | modified_points = tmp_points[ind2:ind1] + add_points
652 | elif index < ind2:
653 | modified_points = tmp_points[ind2:ind1] + add_points
654 | self.current = None
655 | self.modified_memory = None
656 | self.hShape.points = modified_points
657 | self.storeShapes()
658 | self.setHiding()
659 | self.setHiding(False)
660 | self.update()
661 | self.actions.save.setEnabled(True)
662 | group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
663 | self.selectShapePoint(pos, multiple_selection_mode=group_mode)
664 | self.prevPoint = pos
665 | self.repaint()
666 |
667 | # elif ev.button() == QtCore.Qt.RightButton and self.editing():
668 | # group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier
669 | # if not self.selectedShapes or (
670 | # self.hShape is not None
671 | # and self.hShape not in self.selectedShapes
672 | # ):
673 | # self.selectShapePoint(pos, multiple_selection_mode=group_mode)
674 | # self.repaint()
675 | # self.prevPoint = pos
676 |
677 | def mouseReleaseEvent(self, ev):
678 | if ev.button() == QtCore.Qt.RightButton:
679 | pass
680 | # menu = self.menus[len(self.selectedShapesCopy) > 0]
681 | # self.restoreCursor()
682 | # if (
683 | # not menu.exec_(self.mapToGlobal(ev.pos()))
684 | # and self.selectedShapesCopy
685 | # ):
686 | # # Cancel the move by deleting the shadow copy.
687 | # self.selectedShapesCopy = []
688 | # self.repaint()
689 | elif ev.button() == QtCore.Qt.LeftButton:
690 | if self.editing():
691 | if (
692 | self.hShape is not None
693 | and self.hShapeIsSelected
694 | and not self.movingShape
695 | ):
696 | self.selectionChanged.emit(
697 | [x for x in self.selectedShapes if x != self.hShape]
698 | )
699 |
700 | if self.movingShape and self.hShape:
701 | index = self.shapes.index(self.hShape)
702 | if (
703 | self.shapesBackups[-1][index].points
704 | != self.shapes[index].points
705 | ):
706 | self.storeShapes()
707 | self.shapeMoved.emit()
708 |
709 | self.movingShape = False
710 |
711 | def endMove(self, copy):
712 | assert self.selectedShapes and self.selectedShapesCopy
713 | assert len(self.selectedShapesCopy) == len(self.selectedShapes)
714 | if copy:
715 | for i, shape in enumerate(self.selectedShapesCopy):
716 | self.shapes.append(shape)
717 | self.selectedShapes[i].selected = False
718 | self.selectedShapes[i] = shape
719 | else:
720 | for i, shape in enumerate(self.selectedShapesCopy):
721 | self.selectedShapes[i].points = shape.points
722 | self.selectedShapesCopy = []
723 | self.repaint()
724 | self.storeShapes()
725 | return True
726 |
727 | def hideBackroundShapes(self, value):
728 | self.hideBackround = value
729 | if self.selectedShapes:
730 | # Only hide other shapes if there is a current selection.
731 | # Otherwise the user will not be able to select a shape.
732 | self.setHiding(True)
733 | self.update()
734 |
735 | def setHiding(self, enable=True):
736 | self._hideBackround = self.hideBackround if enable else False
737 |
738 | def canCloseShape(self):
739 | return self.drawing() and self.current and len(self.current) > 2
740 |
741 | def mouseDoubleClickEvent(self, ev):
742 | # We need at least 4 points here, since the mousePress handler
743 | # adds an extra one before this handler is called.
744 | if (
745 | self.double_click == "close"
746 | and self.canCloseShape()
747 | and len(self.current) > 3
748 | ):
749 | self.current.popPoint()
750 | self.finalise()
751 |
752 | def selectShapes(self, shapes):
753 | self.setHiding()
754 | self.selectionChanged.emit(shapes)
755 | self.update()
756 |
757 | def selectShapePoint(self, point, multiple_selection_mode):
758 | """Select the first shape created which contains this point."""
759 | if self.selectedVertex(): # A vertex is marked for selection.
760 | index, shape = self.hVertex, self.hShape
761 | shape.highlightVertex(index, shape.MOVE_VERTEX)
762 | else:
763 | for shape in reversed(self.shapes):
764 | if self.isVisible(shape) and shape.containsPoint(point):
765 | self.setHiding()
766 | if shape not in self.selectedShapes:
767 | if multiple_selection_mode:
768 | self.selectionChanged.emit(
769 | self.selectedShapes + [shape]
770 | )
771 | else:
772 | self.selectionChanged.emit([shape])
773 | self.hShapeIsSelected = False
774 | else:
775 | self.hShapeIsSelected = True
776 | self.calculateOffsets(point)
777 | return
778 | self.deSelectShape()
779 |
780 | def calculateOffsets(self, point):
781 | left = self.pixmap.width() - 1
782 | right = 0
783 | top = self.pixmap.height() - 1
784 | bottom = 0
785 | for s in self.selectedShapes:
786 | rect = s.boundingRect()
787 | if rect.left() < left:
788 | left = rect.left()
789 | if rect.right() > right:
790 | right = rect.right()
791 | if rect.top() < top:
792 | top = rect.top()
793 | if rect.bottom() > bottom:
794 | bottom = rect.bottom()
795 |
796 | x1 = left - point.x()
797 | y1 = top - point.y()
798 | x2 = right - point.x()
799 | y2 = bottom - point.y()
800 | self.offsets = QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)
801 |
802 | def boundedMoveVertex(self, pos):
803 | index, shape = self.hVertex, self.hShape
804 | point = shape[index]
805 | if self.outOfPixmap(pos):
806 | pos = self.intersectionPoint(point, pos)
807 | shape.moveVertexBy(index, pos - point)
808 |
809 | def boundedMoveShapes(self, shapes, pos):
810 | if self.outOfPixmap(pos):
811 | return False # No need to move
812 | o1 = pos + self.offsets[0]
813 | if self.outOfPixmap(o1):
814 | pos -= QtCore.QPoint(min(0, o1.x()), min(0, o1.y()))
815 | o2 = pos + self.offsets[1]
816 | if self.outOfPixmap(o2):
817 | pos += QtCore.QPoint(
818 | min(0, self.pixmap.width() - o2.x()),
819 | min(0, self.pixmap.height() - o2.y()),
820 | )
821 | # XXX: The next line tracks the new position of the cursor
822 | # relative to the shape, but also results in making it
823 | # a bit "shaky" when nearing the border and allows it to
824 | # go outside of the shape's area for some reason.
825 | # self.calculateOffsets(self.selectedShapes, pos)
826 | dp = pos - self.prevPoint
827 | if dp:
828 | for shape in shapes:
829 | shape.moveBy(dp)
830 | self.prevPoint = pos
831 | return True
832 | return False
833 |
834 | def deSelectShape(self):
835 | if self.selectedShapes:
836 | self.setHiding(False)
837 | self.selectionChanged.emit([])
838 | self.hShapeIsSelected = False
839 | self.update()
840 |
841 | def deleteSelected(self):
842 | deleted_shapes = []
843 | if self.selectedShapes:
844 | try:
845 | for shape in self.selectedShapes:
846 | self.shapes.remove(shape)
847 | deleted_shapes.append(shape)
848 | except:
849 | pass
850 | self.storeShapes()
851 | self.selectedShapes = []
852 | self.update()
853 | return deleted_shapes
854 |
855 | def deleteShape(self, shape):
856 | if shape in self.selectedShapes:
857 | self.selectedShapes.remove(shape)
858 | if shape in self.shapes:
859 | self.shapes.remove(shape)
860 | self.storeShapes()
861 | self.update()
862 |
863 | def duplicateSelectedShapes(self):
864 | if self.selectedShapes:
865 | self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]
866 | self.boundedShiftShapes(self.selectedShapesCopy)
867 | self.endMove(copy=True)
868 | return self.selectedShapes
869 |
870 | def boundedShiftShapes(self, shapes):
871 | # Try to move in one direction, and if it fails in another.
872 | # Give up if both fail.
873 | point = shapes[0][0]
874 | offset = QtCore.QPointF(2.0, 2.0)
875 | self.offsets = QtCore.QPoint(), QtCore.QPoint()
876 | self.prevPoint = point
877 | if not self.boundedMoveShapes(shapes, point - offset):
878 | self.boundedMoveShapes(shapes, point + offset)
879 |
880 | def paintEvent(self, event):
881 | if not self.pixmap:
882 | return super(Canvas, self).paintEvent(event)
883 |
884 | p = self._painter
885 | p.begin(self)
886 | p.setRenderHint(QtGui.QPainter.Antialiasing)
887 | p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
888 | p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
889 |
890 | p.scale(self.scale, self.scale)
891 | p.translate(self.offsetToCenter())
892 |
893 | p.drawPixmap(0, 0, self.pixmap)
894 |
895 | # draw crosshair
896 | if (
897 | self._crosshair[self._createMode]
898 | and self.drawing()
899 | and self.prevMovePoint
900 | and not self.outOfPixmap(self.prevMovePoint)
901 | ):
902 | p.setPen(QtGui.QColor(0, 0, 0))
903 | p.drawLine(
904 | 0,
905 | int(self.prevMovePoint.y()),
906 | self.width() - 1,
907 | int(self.prevMovePoint.y()),
908 | )
909 | p.drawLine(
910 | int(self.prevMovePoint.x()),
911 | 0,
912 | int(self.prevMovePoint.x()),
913 | self.height() - 1,
914 | )
915 |
916 | Shape.scale = self.scale
917 | for shape in self.shapes:
918 | if (shape.selected or not self._hideBackround) and self.isVisible(
919 | shape
920 | ):
921 | shape.fill = shape.selected or shape == self.hShape
922 | shape.paint(p)
923 | if self.current:
924 | self.current.paint(p)
925 | if self.currentPos:
926 | self.currentPos.paint(p,flag=1)
927 | if self.currentNeg:
928 | self.currentNeg.paint(p,flag=0)
929 | if self.currentBox:
930 | self.currentBox.paint(p)
931 | if self.selectedShapesCopy:
932 | for s in self.selectedShapesCopy:
933 | s.paint(p)
934 |
935 | if (
936 | self.fillDrawing()
937 | and self.createMode == "polygon"
938 | and self.current is not None
939 | and len(self.current.points) >= 2
940 | ):
941 | drawing_shape = self.current.copy()
942 | drawing_shape.addPoint(self.line[1])
943 | drawing_shape.fill = True
944 | drawing_shape.paint(p)
945 |
946 | if len(self.app.sam_mask) > 0:
947 | for tmp_mask in self.app.sam_mask:
948 | drawing_shape = tmp_mask.copy()
949 | drawing_shape.fill = True
950 | drawing_shape.paint(p, proposal_flag=1)
951 | p.end()
952 |
953 | def transformPos(self, point):
954 | """Convert from widget-logical coordinates to painter-logical ones."""
955 | return point / self.scale - self.offsetToCenter()
956 |
957 | def offsetToCenter(self):
958 | s = self.scale
959 | area = super(Canvas, self).size()
960 | w, h = self.pixmap.width() * s, self.pixmap.height() * s
961 | aw, ah = area.width(), area.height()
962 | x = (aw - w) / (2 * s) if aw > w else 0
963 | y = (ah - h) / (2 * s) if ah > h else 0
964 | return QtCore.QPointF(x, y)
965 |
966 | def outOfPixmap(self, p):
967 | w, h = self.pixmap.width(), self.pixmap.height()
968 | return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1)
969 |
970 | def finalise(self):
971 | assert self.current
972 | self.current.close()
973 | self.shapes.append(self.current)
974 | self.storeShapes()
975 | self.current = None
976 | self.currentPos = None
977 | self.currentNeg = None
978 | self.currentBox = None
979 | self.setHiding(False)
980 | self.newShape.emit()
981 | self.update()
982 |
983 | def finaliseBox(self):
984 | assert self.currentBox
985 | self.currentBox.close()
986 | self.shapes.append(self.currentBox)
987 | self.storeShapes()
988 | self.current = None
989 | self.currentPos = None
990 | self.currentNeg = None
991 | self.currentBox = None
992 | self.setHiding(False)
993 | self.newShape.emit()
994 | self.update()
995 |
996 | def closeEnough(self, p1, p2):
997 | # d = distance(p1 - p2)
998 | # m = (p1-p2).manhattanLength()
999 | # print "d %.2f, m %d, %.2f" % (d, m, d - m)
1000 | # divide by scale to allow more precision when zoomed in
1001 | return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale)
1002 |
1003 | def intersectionPoint(self, p1, p2):
1004 | # Cycle through each image edge in clockwise fashion,
1005 | # and find the one intersecting the current line segment.
1006 | # http://paulbourke.net/geometry/lineline2d/
1007 | size = self.pixmap.size()
1008 | points = [
1009 | (0, 0),
1010 | (size.width() - 1, 0),
1011 | (size.width() - 1, size.height() - 1),
1012 | (0, size.height() - 1),
1013 | ]
1014 | # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap
1015 | x1 = min(max(p1.x(), 0), size.width() - 1)
1016 | y1 = min(max(p1.y(), 0), size.height() - 1)
1017 | x2, y2 = p2.x(), p2.y()
1018 | d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
1019 | x3, y3 = points[i]
1020 | x4, y4 = points[(i + 1) % 4]
1021 | if (x, y) == (x1, y1):
1022 | # Handle cases where previous point is on one of the edges.
1023 | if x3 == x4:
1024 | return QtCore.QPointF(x3, min(max(0, y2), max(y3, y4)))
1025 | else: # y3 == y4
1026 | return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3)
1027 | return QtCore.QPointF(x, y)
1028 |
1029 | def intersectingEdges(self, point1, point2, points):
1030 | """Find intersecting edges.
1031 |
1032 | For each edge formed by `points', yield the intersection
1033 | with the line segment `(x1,y1) - (x2,y2)`, if it exists.
1034 | Also return the distance of `(x2,y2)' to the middle of the
1035 | edge along with its index, so that the one closest can be chosen.
1036 | """
1037 | (x1, y1) = point1
1038 | (x2, y2) = point2
1039 | for i in range(4):
1040 | x3, y3 = points[i]
1041 | x4, y4 = points[(i + 1) % 4]
1042 | denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
1043 | nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
1044 | nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
1045 | if denom == 0:
1046 | # This covers two cases:
1047 | # nua == nub == 0: Coincident
1048 | # otherwise: Parallel
1049 | continue
1050 | ua, ub = nua / denom, nub / denom
1051 | if 0 <= ua <= 1 and 0 <= ub <= 1:
1052 | x = x1 + ua * (x2 - x1)
1053 | y = y1 + ua * (y2 - y1)
1054 | m = QtCore.QPointF((x3 + x4) / 2, (y3 + y4) / 2)
1055 | d = labelme.utils.distance(m - QtCore.QPointF(x2, y2))
1056 | yield d, i, (x, y)
1057 |
1058 | # These two, along with a call to adjustSize are required for the
1059 | # scroll area.
1060 | def sizeHint(self):
1061 | return self.minimumSizeHint()
1062 |
1063 | def minimumSizeHint(self):
1064 | if self.pixmap:
1065 | return self.scale * self.pixmap.size()
1066 | return super(Canvas, self).minimumSizeHint()
1067 |
1068 | def wheelEvent(self, ev):
1069 | if QT5:
1070 | mods = ev.modifiers()
1071 | delta = ev.angleDelta()
1072 | if QtCore.Qt.ControlModifier == int(mods):
1073 | # with Ctrl/Command key
1074 | # zoom
1075 | self.zoomRequest.emit(delta.y(), ev.pos())
1076 | else:
1077 | # scroll
1078 | self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)
1079 | self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)
1080 | else:
1081 | if ev.orientation() == QtCore.Qt.Vertical:
1082 | mods = ev.modifiers()
1083 | if QtCore.Qt.ControlModifier == int(mods):
1084 | # with Ctrl/Command key
1085 | self.zoomRequest.emit(ev.delta(), ev.pos())
1086 | else:
1087 | self.scrollRequest.emit(
1088 | ev.delta(),
1089 | QtCore.Qt.Horizontal
1090 | if (QtCore.Qt.ShiftModifier == int(mods))
1091 | else QtCore.Qt.Vertical,
1092 | )
1093 | else:
1094 | self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)
1095 | ev.accept()
1096 |
1097 | def moveByKeyboard(self, offset):
1098 | if self.selectedShapes:
1099 | self.boundedMoveShapes(
1100 | self.selectedShapes, self.prevPoint + offset
1101 | )
1102 | self.repaint()
1103 | self.movingShape = True
1104 |
1105 | def keyPressEvent(self, ev):
1106 | modifiers = ev.modifiers()
1107 | key = ev.key()
1108 | if self.drawing():
1109 | if key == QtCore.Qt.Key_Escape and self.current:
1110 | self.current = None
1111 | self.drawingPolygon.emit(False)
1112 | self.update()
1113 | elif key == QtCore.Qt.Key_Return and self.canCloseShape():
1114 | self.finalise()
1115 | elif modifiers == QtCore.Qt.AltModifier:
1116 | self.snapping = False
1117 | elif self.editing():
1118 | if key == QtCore.Qt.Key_Up:
1119 | self.moveByKeyboard(QtCore.QPointF(0.0, -MOVE_SPEED))
1120 | elif key == QtCore.Qt.Key_Down:
1121 | self.moveByKeyboard(QtCore.QPointF(0.0, MOVE_SPEED))
1122 | elif key == QtCore.Qt.Key_Left:
1123 | self.moveByKeyboard(QtCore.QPointF(-MOVE_SPEED, 0.0))
1124 | elif key == QtCore.Qt.Key_Right:
1125 | self.moveByKeyboard(QtCore.QPointF(MOVE_SPEED, 0.0))
1126 |
1127 | def keyReleaseEvent(self, ev):
1128 | modifiers = ev.modifiers()
1129 | if self.drawing():
1130 | if int(modifiers) == 0:
1131 | self.snapping = True
1132 | elif self.editing():
1133 | if self.movingShape and self.selectedShapes:
1134 | index = self.shapes.index(self.selectedShapes[0])
1135 | if (
1136 | self.shapesBackups[-1][index].points
1137 | != self.shapes[index].points
1138 | ):
1139 | self.storeShapes()
1140 | self.shapeMoved.emit()
1141 |
1142 | self.movingShape = False
1143 |
1144 | def setLastLabel(self, text, flags):
1145 | assert text
1146 | self.shapes[-1].label = text
1147 | self.shapes[-1].flags = flags
1148 | self.shapesBackups.pop()
1149 | self.storeShapes()
1150 | return self.shapes[-1]
1151 |
1152 | def undoLastLine(self):
1153 | assert self.shapes
1154 | self.current = self.shapes.pop()
1155 | self.current.setOpen()
1156 | if self.createMode in ["polygon", "linestrip"]:
1157 | self.line.points = [self.current[-1], self.current[0]]
1158 | elif self.createMode in ["rectangle", "line", "circle"]:
1159 | self.current.points = self.current.points[0:1]
1160 | elif self.createMode == "point":
1161 | self.current = None
1162 | self.drawingPolygon.emit(True)
1163 |
1164 | def undoLastPoint(self):
1165 | if not self.current or self.current.isClosed():
1166 | return
1167 | self.current.popPoint()
1168 | if len(self.current) > 0:
1169 | self.line[0] = self.current[-1]
1170 | else:
1171 | self.current = None
1172 | self.drawingPolygon.emit(False)
1173 | self.update()
1174 |
1175 | def loadPixmap(self, pixmap, clear_shapes=True):
1176 | self.pixmap = pixmap
1177 | if clear_shapes:
1178 | self.shapes = []
1179 | self.update()
1180 |
1181 | def loadShapes(self, shapes, replace=True):
1182 | if replace:
1183 | self.shapes = list(shapes)
1184 | else:
1185 | self.shapes.extend(shapes)
1186 | self.storeShapes()
1187 | self.current = None
1188 | self.hShape = None
1189 | self.hVertex = None
1190 | self.hEdge = None
1191 | self.update()
1192 |
1193 | def setShapeVisible(self, shape, value):
1194 | self.visible[shape] = value
1195 | self.update()
1196 |
1197 | def overrideCursor(self, cursor):
1198 | self.restoreCursor()
1199 | self._cursor = cursor
1200 | QtWidgets.QApplication.setOverrideCursor(cursor)
1201 |
1202 | def restoreCursor(self):
1203 | QtWidgets.QApplication.restoreOverrideCursor()
1204 |
1205 | def resetState(self):
1206 | self.restoreCursor()
1207 | self.pixmap = None
1208 | self.shapesBackups = []
1209 | self.update()
1210 |
1211 |
1212 |
1213 |
--------------------------------------------------------------------------------
/categories.txt:
--------------------------------------------------------------------------------
1 | aerosol_can
2 | air_conditioner
3 | airplane
4 | alarm_clock
5 | alcohol
6 | alligator
7 | almond
8 | ambulance
9 | amplifier
10 | anklet
11 | antenna
12 | apple
13 | applesauce
14 | apricot
15 | apron
16 | aquarium
17 | arctic_(type_of_shoe)
18 | armband
19 | armchair
20 | armoire
21 | armor
22 | artichoke
23 | trash_can
24 | ashtray
25 | asparagus
26 | atomizer
27 | avocado
28 | award
29 | awning
30 | ax
31 | baboon
32 | baby_buggy
33 | basketball_backboard
34 | backpack
35 | handbag
36 | suitcase
37 | bagel
38 | bagpipe
39 | baguet
40 | bait
41 | ball
42 | ballet_skirt
43 | balloon
44 | bamboo
45 | banana
46 | Band_Aid
47 | bandage
48 | bandanna
49 | banjo
50 | banner
51 | barbell
52 | barge
53 | barrel
54 | barrette
55 | barrow
56 | baseball_base
57 | baseball
58 | baseball_bat
59 | baseball_cap
60 | baseball_glove
61 | basket
62 | basketball
63 | bass_horn
64 | bat_(animal)
65 | bath_mat
66 | bath_towel
67 | bathrobe
68 | bathtub
69 | batter_(food)
70 | battery
71 | beachball
72 | bead
73 | bean_curd
74 | beanbag
75 | beanie
76 | bear
77 | bed
78 | bedpan
79 | bedspread
80 | cow
81 | beef_(food)
82 | beeper
83 | beer_bottle
84 | beer_can
85 | beetle
86 | bell
87 | bell_pepper
88 | belt
89 | belt_buckle
90 | bench
91 | beret
92 | bib
93 | Bible
94 | bicycle
95 | visor
96 | billboard
97 | binder
98 | binoculars
99 | bird
100 | birdfeeder
101 | birdbath
102 | birdcage
103 | birdhouse
104 | birthday_cake
105 | birthday_card
106 | pirate_flag
107 | black_sheep
108 | blackberry
109 | blackboard
110 | blanket
111 | blazer
112 | blender
113 | blimp
114 | blinker
115 | blouse
116 | blueberry
117 | gameboard
118 | boat
119 | bob
120 | bobbin
121 | bobby_pin
122 | boiled_egg
123 | bolo_tie
124 | deadbolt
125 | bolt
126 | bonnet
127 | book
128 | bookcase
129 | booklet
130 | bookmark
131 | boom_microphone
132 | boot
133 | bottle
134 | bottle_opener
135 | bouquet
136 | bow_(weapon)
137 | bow_(decorative_ribbons)
138 | bow-tie
139 | bowl
140 | pipe_bowl
141 | bowler_hat
142 | bowling_ball
143 | box
144 | boxing_glove
145 | suspenders
146 | bracelet
147 | brass_plaque
148 | brassiere
149 | bread-bin
150 | bread
151 | breechcloth
152 | bridal_gown
153 | briefcase
154 | broccoli
155 | broach
156 | broom
157 | brownie
158 | brussels_sprouts
159 | bubble_gum
160 | bucket
161 | horse_buggy
162 | bull
163 | bulldog
164 | bulldozer
165 | bullet_train
166 | bulletin_board
167 | bulletproof_vest
168 | bullhorn
169 | bun
170 | bunk_bed
171 | buoy
172 | burrito
173 | bus_(vehicle)
174 | business_card
175 | butter
176 | butterfly
177 | button
178 | cab_(taxi)
179 | cabana
180 | cabin_car
181 | cabinet
182 | locker
183 | cake
184 | calculator
185 | calendar
186 | calf
187 | camcorder
188 | camel
189 | camera
190 | camera_lens
191 | camper_(vehicle)
192 | can
193 | can_opener
194 | candle
195 | candle_holder
196 | candy_bar
197 | candy_cane
198 | walking_cane
199 | canister
200 | canoe
201 | cantaloup
202 | canteen
203 | cap_(headwear)
204 | bottle_cap
205 | cape
206 | cappuccino
207 | car_(automobile)
208 | railcar_(part_of_a_train)
209 | elevator_car
210 | car_battery
211 | identity_card
212 | card
213 | cardigan
214 | cargo_ship
215 | carnation
216 | horse_carriage
217 | carrot
218 | tote_bag
219 | cart
220 | carton
221 | cash_register
222 | casserole
223 | cassette
224 | cast
225 | cat
226 | cauliflower
227 | cayenne_(spice)
228 | CD_player
229 | celery
230 | cellular_telephone
231 | chain_mail
232 | chair
233 | chaise_longue
234 | chalice
235 | chandelier
236 | chap
237 | checkbook
238 | checkerboard
239 | cherry
240 | chessboard
241 | chicken_(animal)
242 | chickpea
243 | chili_(vegetable)
244 | chime
245 | chinaware
246 | crisp_(potato_chip)
247 | poker_chip
248 | chocolate_bar
249 | chocolate_cake
250 | chocolate_milk
251 | chocolate_mousse
252 | choker
253 | chopping_board
254 | chopstick
255 | Christmas_tree
256 | slide
257 | cider
258 | cigar_box
259 | cigarette
260 | cigarette_case
261 | cistern
262 | clarinet
263 | clasp
264 | cleansing_agent
265 | cleat_(for_securing_rope)
266 | clementine
267 | clip
268 | clipboard
269 | clippers_(for_plants)
270 | cloak
271 | clock
272 | clock_tower
273 | clothes_hamper
274 | clothespin
275 | clutch_bag
276 | coaster
277 | coat
278 | coat_hanger
279 | coatrack
280 | cock
281 | cockroach
282 | cocoa_(beverage)
283 | coconut
284 | coffee_maker
285 | coffee_table
286 | coffeepot
287 | coil
288 | coin
289 | colander
290 | coleslaw
291 | coloring_material
292 | combination_lock
293 | pacifier
294 | comic_book
295 | compass
296 | computer_keyboard
297 | condiment
298 | cone
299 | control
300 | convertible_(automobile)
301 | sofa_bed
302 | cooker
303 | cookie
304 | cooking_utensil
305 | cooler_(for_food)
306 | cork_(bottle_plug)
307 | corkboard
308 | corkscrew
309 | edible_corn
310 | cornbread
311 | cornet
312 | cornice
313 | cornmeal
314 | corset
315 | costume
316 | cougar
317 | coverall
318 | cowbell
319 | cowboy_hat
320 | crab_(animal)
321 | crabmeat
322 | cracker
323 | crape
324 | crate
325 | crayon
326 | cream_pitcher
327 | crescent_roll
328 | crib
329 | crock_pot
330 | crossbar
331 | crouton
332 | crow
333 | crowbar
334 | crown
335 | crucifix
336 | cruise_ship
337 | police_cruiser
338 | crumb
339 | crutch
340 | cub_(animal)
341 | cube
342 | cucumber
343 | cufflink
344 | cup
345 | trophy_cup
346 | cupboard
347 | cupcake
348 | hair_curler
349 | curling_iron
350 | curtain
351 | cushion
352 | cylinder
353 | cymbal
354 | dagger
355 | dalmatian
356 | dartboard
357 | date_(fruit)
358 | deck_chair
359 | deer
360 | dental_floss
361 | desk
362 | detergent
363 | diaper
364 | diary
365 | die
366 | dinghy
367 | dining_table
368 | tux
369 | dish
370 | dish_antenna
371 | dishrag
372 | dishtowel
373 | dishwasher
374 | dishwasher_detergent
375 | dispenser
376 | diving_board
377 | Dixie_cup
378 | dog
379 | dog_collar
380 | doll
381 | dollar
382 | dollhouse
383 | dolphin
384 | domestic_ass
385 | doorknob
386 | doormat
387 | doughnut
388 | dove
389 | dragonfly
390 | drawer
391 | underdrawers
392 | dress
393 | dress_hat
394 | dress_suit
395 | dresser
396 | drill
397 | drone
398 | dropper
399 | drum_(musical_instrument)
400 | drumstick
401 | duck
402 | duckling
403 | duct_tape
404 | duffel_bag
405 | dumbbell
406 | dumpster
407 | dustpan
408 | eagle
409 | earphone
410 | earplug
411 | earring
412 | easel
413 | eclair
414 | eel
415 | egg
416 | egg_roll
417 | egg_yolk
418 | eggbeater
419 | eggplant
420 | electric_chair
421 | refrigerator
422 | elephant
423 | elk
424 | envelope
425 | eraser
426 | escargot
427 | eyepatch
428 | falcon
429 | fan
430 | faucet
431 | fedora
432 | ferret
433 | Ferris_wheel
434 | ferry
435 | fig_(fruit)
436 | fighter_jet
437 | figurine
438 | file_cabinet
439 | file_(tool)
440 | fire_alarm
441 | fire_engine
442 | fire_extinguisher
443 | fire_hose
444 | fireplace
445 | fireplug
446 | first-aid_kit
447 | fish
448 | fish_(food)
449 | fishbowl
450 | fishing_rod
451 | flag
452 | flagpole
453 | flamingo
454 | flannel
455 | flap
456 | flash
457 | flashlight
458 | fleece
459 | flip-flop_(sandal)
460 | flipper_(footwear)
461 | flower_arrangement
462 | flute_glass
463 | foal
464 | folding_chair
465 | food_processor
466 | football_(American)
467 | football_helmet
468 | footstool
469 | fork
470 | forklift
471 | freight_car
472 | French_toast
473 | freshener
474 | frisbee
475 | frog
476 | fruit_juice
477 | frying_pan
478 | fudge
479 | funnel
480 | futon
481 | gag
482 | garbage
483 | garbage_truck
484 | garden_hose
485 | gargle
486 | gargoyle
487 | garlic
488 | gasmask
489 | gazelle
490 | gelatin
491 | gemstone
492 | generator
493 | giant_panda
494 | gift_wrap
495 | ginger
496 | giraffe
497 | cincture
498 | glass_(drink_container)
499 | globe
500 | glove
501 | goat
502 | goggles
503 | goldfish
504 | golf_club
505 | golfcart
506 | gondola_(boat)
507 | goose
508 | gorilla
509 | gourd
510 | grape
511 | grater
512 | gravestone
513 | gravy_boat
514 | green_bean
515 | green_onion
516 | griddle
517 | grill
518 | grits
519 | grizzly
520 | grocery_bag
521 | guitar
522 | gull
523 | gun
524 | hairbrush
525 | hairnet
526 | hairpin
527 | halter_top
528 | ham
529 | hamburger
530 | hammer
531 | hammock
532 | hamper
533 | hamster
534 | hair_dryer
535 | hand_glass
536 | hand_towel
537 | handcart
538 | handcuff
539 | handkerchief
540 | handle
541 | handsaw
542 | hardback_book
543 | harmonium
544 | hat
545 | hatbox
546 | veil
547 | headband
548 | headboard
549 | headlight
550 | headscarf
551 | headset
552 | headstall_(for_horses)
553 | heart
554 | heater
555 | helicopter
556 | helmet
557 | heron
558 | highchair
559 | hinge
560 | hippopotamus
561 | hockey_stick
562 | hog
563 | home_plate_(baseball)
564 | honey
565 | fume_hood
566 | hook
567 | hookah
568 | hornet
569 | horse
570 | hose
571 | hot-air_balloon
572 | hotplate
573 | hot_sauce
574 | hourglass
575 | houseboat
576 | hummingbird
577 | hummus
578 | polar_bear
579 | icecream
580 | popsicle
581 | ice_maker
582 | ice_pack
583 | ice_skate
584 | igniter
585 | inhaler
586 | iPod
587 | iron_(for_clothing)
588 | ironing_board
589 | jacket
590 | jam
591 | jar
592 | jean
593 | jeep
594 | jelly_bean
595 | jersey
596 | jet_plane
597 | jewel
598 | jewelry
599 | joystick
600 | jumpsuit
601 | kayak
602 | keg
603 | kennel
604 | kettle
605 | key
606 | keycard
607 | kilt
608 | kimono
609 | kitchen_sink
610 | kitchen_table
611 | kite
612 | kitten
613 | kiwi_fruit
614 | knee_pad
615 | knife
616 | knitting_needle
617 | knob
618 | knocker_(on_a_door)
619 | koala
620 | lab_coat
621 | ladder
622 | ladle
623 | ladybug
624 | lamb_(animal)
625 | lamb-chop
626 | lamp
627 | lamppost
628 | lampshade
629 | lantern
630 | lanyard
631 | laptop_computer
632 | lasagna
633 | latch
634 | lawn_mower
635 | leather
636 | legging_(clothing)
637 | Lego
638 | legume
639 | lemon
640 | lemonade
641 | lettuce
642 | license_plate
643 | life_buoy
644 | life_jacket
645 | lightbulb
646 | lightning_rod
647 | lime
648 | limousine
649 | lion
650 | lip_balm
651 | liquor
652 | lizard
653 | log
654 | lollipop
655 | speaker_(stero_equipment)
656 | loveseat
657 | machine_gun
658 | magazine
659 | magnet
660 | mail_slot
661 | mailbox_(at_home)
662 | mallard
663 | mallet
664 | mammoth
665 | manatee
666 | mandarin_orange
667 | manger
668 | manhole
669 | map
670 | marker
671 | martini
672 | mascot
673 | mashed_potato
674 | masher
675 | mask
676 | mast
677 | mat_(gym_equipment)
678 | matchbox
679 | mattress
680 | measuring_cup
681 | measuring_stick
682 | meatball
683 | medicine
684 | melon
685 | microphone
686 | microscope
687 | microwave_oven
688 | milestone
689 | milk
690 | milk_can
691 | milkshake
692 | minivan
693 | mint_candy
694 | mirror
695 | mitten
696 | mixer_(kitchen_tool)
697 | money
698 | monitor_(computer_equipment)
699 | monkey
700 | motor
701 | motor_scooter
702 | motor_vehicle
703 | motorcycle
704 | mound_(baseball)
705 | mouse_(computer_equipment)
706 | mousepad
707 | muffin
708 | mug
709 | mushroom
710 | music_stool
711 | musical_instrument
712 | nailfile
713 | napkin
714 | neckerchief
715 | necklace
716 | necktie
717 | needle
718 | nest
719 | newspaper
720 | newsstand
721 | nightshirt
722 | nosebag_(for_animals)
723 | noseband_(for_animals)
724 | notebook
725 | notepad
726 | nut
727 | nutcracker
728 | oar
729 | octopus_(food)
730 | octopus_(animal)
731 | oil_lamp
732 | olive_oil
733 | omelet
734 | onion
735 | orange_(fruit)
736 | orange_juice
737 | ostrich
738 | ottoman
739 | oven
740 | overalls_(clothing)
741 | owl
742 | packet
743 | inkpad
744 | pad
745 | paddle
746 | padlock
747 | paintbrush
748 | painting
749 | pajamas
750 | palette
751 | pan_(for_cooking)
752 | pan_(metal_container)
753 | pancake
754 | pantyhose
755 | papaya
756 | paper_plate
757 | paper_towel
758 | paperback_book
759 | paperweight
760 | parachute
761 | parakeet
762 | parasail_(sports)
763 | parasol
764 | parchment
765 | parka
766 | parking_meter
767 | parrot
768 | passenger_car_(part_of_a_train)
769 | passenger_ship
770 | passport
771 | pastry
772 | patty_(food)
773 | pea_(food)
774 | peach
775 | peanut_butter
776 | pear
777 | peeler_(tool_for_fruit_and_vegetables)
778 | wooden_leg
779 | pegboard
780 | pelican
781 | pen
782 | pencil
783 | pencil_box
784 | pencil_sharpener
785 | pendulum
786 | penguin
787 | pennant
788 | penny_(coin)
789 | pepper
790 | pepper_mill
791 | perfume
792 | persimmon
793 | person
794 | pet
795 | pew_(church_bench)
796 | phonebook
797 | phonograph_record
798 | piano
799 | pickle
800 | pickup_truck
801 | pie
802 | pigeon
803 | piggy_bank
804 | pillow
805 | pin_(non_jewelry)
806 | pineapple
807 | pinecone
808 | ping-pong_ball
809 | pinwheel
810 | tobacco_pipe
811 | pipe
812 | pistol
813 | pita_(bread)
814 | pitcher_(vessel_for_liquid)
815 | pitchfork
816 | pizza
817 | place_mat
818 | plate
819 | platter
820 | playpen
821 | pliers
822 | plow_(farm_equipment)
823 | plume
824 | pocket_watch
825 | pocketknife
826 | poker_(fire_stirring_tool)
827 | pole
828 | polo_shirt
829 | poncho
830 | pony
831 | pool_table
832 | pop_(soda)
833 | postbox_(public)
834 | postcard
835 | poster
836 | pot
837 | flowerpot
838 | potato
839 | potholder
840 | pottery
841 | pouch
842 | power_shovel
843 | prawn
844 | pretzel
845 | printer
846 | projectile_(weapon)
847 | projector
848 | propeller
849 | prune
850 | pudding
851 | puffer_(fish)
852 | puffin
853 | pug-dog
854 | pumpkin
855 | puncher
856 | puppet
857 | puppy
858 | quesadilla
859 | quiche
860 | quilt
861 | rabbit
862 | race_car
863 | racket
864 | radar
865 | radiator
866 | radio_receiver
867 | radish
868 | raft
869 | rag_doll
870 | raincoat
871 | ram_(animal)
872 | raspberry
873 | rat
874 | razorblade
875 | reamer_(juicer)
876 | rearview_mirror
877 | receipt
878 | recliner
879 | record_player
880 | reflector
881 | remote_control
882 | rhinoceros
883 | rib_(food)
884 | rifle
885 | ring
886 | river_boat
887 | road_map
888 | robe
889 | rocking_chair
890 | rodent
891 | roller_skate
892 | Rollerblade
893 | rolling_pin
894 | root_beer
895 | router_(computer_equipment)
896 | rubber_band
897 | runner_(carpet)
898 | plastic_bag
899 | saddle_(on_an_animal)
900 | saddle_blanket
901 | saddlebag
902 | safety_pin
903 | sail
904 | salad
905 | salad_plate
906 | salami
907 | salmon_(fish)
908 | salmon_(food)
909 | salsa
910 | saltshaker
911 | sandal_(type_of_shoe)
912 | sandwich
913 | satchel
914 | saucepan
915 | saucer
916 | sausage
917 | sawhorse
918 | saxophone
919 | scale_(measuring_instrument)
920 | scarecrow
921 | scarf
922 | school_bus
923 | scissors
924 | scoreboard
925 | scraper
926 | screwdriver
927 | scrubbing_brush
928 | sculpture
929 | seabird
930 | seahorse
931 | seaplane
932 | seashell
933 | sewing_machine
934 | shaker
935 | shampoo
936 | shark
937 | sharpener
938 | Sharpie
939 | shaver_(electric)
940 | shaving_cream
941 | shawl
942 | shears
943 | sheep
944 | shepherd_dog
945 | sherbert
946 | shield
947 | shirt
948 | shoe
949 | shopping_bag
950 | shopping_cart
951 | short_pants
952 | shot_glass
953 | shoulder_bag
954 | shovel
955 | shower_head
956 | shower_cap
957 | shower_curtain
958 | shredder_(for_paper)
959 | signboard
960 | silo
961 | sink
962 | skateboard
963 | skewer
964 | ski
965 | ski_boot
966 | ski_parka
967 | ski_pole
968 | skirt
969 | skullcap
970 | sled
971 | sleeping_bag
972 | sling_(bandage)
973 | slipper_(footwear)
974 | smoothie
975 | snake
976 | snowboard
977 | snowman
978 | snowmobile
979 | soap
980 | soccer_ball
981 | sock
982 | sofa
983 | softball
984 | solar_array
985 | sombrero
986 | soup
987 | soup_bowl
988 | soupspoon
989 | sour_cream
990 | soya_milk
991 | space_shuttle
992 | sparkler_(fireworks)
993 | spatula
994 | spear
995 | spectacles
996 | spice_rack
997 | spider
998 | crawfish
999 | sponge
1000 | spoon
1001 | sportswear
1002 | spotlight
1003 | squid_(food)
1004 | squirrel
1005 | stagecoach
1006 | stapler_(stapling_machine)
1007 | starfish
1008 | statue_(sculpture)
1009 | steak_(food)
1010 | steak_knife
1011 | steering_wheel
1012 | stepladder
1013 | step_stool
1014 | stereo_(sound_system)
1015 | stew
1016 | stirrer
1017 | stirrup
1018 | stool
1019 | stop_sign
1020 | brake_light
1021 | stove
1022 | strainer
1023 | strap
1024 | straw_(for_drinking)
1025 | strawberry
1026 | street_sign
1027 | streetlight
1028 | string_cheese
1029 | stylus
1030 | subwoofer
1031 | sugar_bowl
1032 | sugarcane_(plant)
1033 | suit_(clothing)
1034 | sunflower
1035 | sunglasses
1036 | sunhat
1037 | surfboard
1038 | sushi
1039 | mop
1040 | sweat_pants
1041 | sweatband
1042 | sweater
1043 | sweatshirt
1044 | sweet_potato
1045 | swimsuit
1046 | sword
1047 | syringe
1048 | Tabasco_sauce
1049 | table-tennis_table
1050 | table
1051 | table_lamp
1052 | tablecloth
1053 | tachometer
1054 | taco
1055 | tag
1056 | taillight
1057 | tambourine
1058 | army_tank
1059 | tank_(storage_vessel)
1060 | tank_top_(clothing)
1061 | tape_(sticky_cloth_or_paper)
1062 | tape_measure
1063 | tapestry
1064 | tarp
1065 | tartan
1066 | tassel
1067 | tea_bag
1068 | teacup
1069 | teakettle
1070 | teapot
1071 | teddy_bear
1072 | telephone
1073 | telephone_booth
1074 | telephone_pole
1075 | telephoto_lens
1076 | television_camera
1077 | television_set
1078 | tennis_ball
1079 | tennis_racket
1080 | tequila
1081 | thermometer
1082 | thermos_bottle
1083 | thermostat
1084 | thimble
1085 | thread
1086 | thumbtack
1087 | tiara
1088 | tiger
1089 | tights_(clothing)
1090 | timer
1091 | tinfoil
1092 | tinsel
1093 | tissue_paper
1094 | toast_(food)
1095 | toaster
1096 | toaster_oven
1097 | toilet
1098 | toilet_tissue
1099 | tomato
1100 | tongs
1101 | toolbox
1102 | toothbrush
1103 | toothpaste
1104 | toothpick
1105 | cover
1106 | tortilla
1107 | tow_truck
1108 | towel
1109 | towel_rack
1110 | toy
1111 | tractor_(farm_equipment)
1112 | traffic_light
1113 | dirt_bike
1114 | trailer_truck
1115 | train_(railroad_vehicle)
1116 | trampoline
1117 | tray
1118 | trench_coat
1119 | triangle_(musical_instrument)
1120 | tricycle
1121 | tripod
1122 | trousers
1123 | truck
1124 | truffle_(chocolate)
1125 | trunk
1126 | vat
1127 | turban
1128 | turkey_(food)
1129 | turnip
1130 | turtle
1131 | turtleneck_(clothing)
1132 | typewriter
1133 | umbrella
1134 | underwear
1135 | unicycle
1136 | urinal
1137 | urn
1138 | vacuum_cleaner
1139 | vase
1140 | vending_machine
1141 | vent
1142 | vest
1143 | videotape
1144 | vinegar
1145 | violin
1146 | vodka
1147 | volleyball
1148 | vulture
1149 | waffle
1150 | waffle_iron
1151 | wagon
1152 | wagon_wheel
1153 | walking_stick
1154 | wall_clock
1155 | wall_socket
1156 | wallet
1157 | walrus
1158 | wardrobe
1159 | washbasin
1160 | automatic_washer
1161 | watch
1162 | water_bottle
1163 | water_cooler
1164 | water_faucet
1165 | water_heater
1166 | water_jug
1167 | water_gun
1168 | water_scooter
1169 | water_ski
1170 | water_tower
1171 | watering_can
1172 | watermelon
1173 | weathervane
1174 | webcam
1175 | wedding_cake
1176 | wedding_ring
1177 | wet_suit
1178 | wheel
1179 | wheelchair
1180 | whipped_cream
1181 | whistle
1182 | wig
1183 | wind_chime
1184 | windmill
1185 | window_box_(for_plants)
1186 | windshield_wiper
1187 | windsock
1188 | wine_bottle
1189 | wine_bucket
1190 | wineglass
1191 | blinder_(for_horses)
1192 | wok
1193 | wolf
1194 | wooden_spoon
1195 | wreath
1196 | wrench
1197 | wristband
1198 | wristlet
1199 | yacht
1200 | yogurt
1201 | yoke_(animal_equipment)
1202 | zebra
1203 | zucchini
1204 | seal
1205 | lobster
1206 | jellyfish
1207 | shrimp
1208 | oyster
1209 | clam
1210 | cuttlefish
1211 | sea_urchin
1212 | moray_eel
1213 | beluga_whale
1214 | blue_whale
1215 | mosquito
1216 | grasshopper
1217 | praying_mantis
1218 | wasp
1219 | termites
1220 | firefly
1221 | cicada
1222 | flea
1223 | scorpions
1224 | centipede
1225 | fox
1226 | skunk
1227 | raccoon
1228 | moose
1229 | antelope
1230 | warthog
1231 | jackal
1232 | cheetah
1233 | jaguar
1234 | snow_leopard
1235 | leopard_cat
1236 | kangaroo
1237 | platypus
1238 | badger
1239 | hedgehog
1240 | mole
1241 | marten
1242 | peacock
1243 | swallow
1244 | sparrow
1245 | seagull
1246 | nightingale
1247 | woodpecker
1248 | magpie
1249 | cockatoo
1250 | mango
1251 | grapefruit
1252 | pomegranate
1253 | cranberry
1254 | cantaloupe
1255 | lychee
1256 | starfruit
1257 | guava
1258 | tangerine
1259 | nectarine
1260 | jackfruit
1261 | quince
1262 | durian
1263 | dragonfruit
1264 | carambola
1265 | passion_fruit
1266 | squash
1267 | cabbage
1268 | okra
1269 | bamboo_shoots
1270 | spinach
1271 | ramen
1272 | scallops
1273 | clams
1274 | tuna
1275 | sardine
1276 | cod
1277 | fried_chicken
1278 | hot_dog
1279 | sandwiches
1280 | weightlifting_belt
1281 | kettlebell
1282 | jump_rope
1283 | yoga_mat
1284 | hand_grips_strengthener
1285 | javelin
1286 | discus
1287 | shot_put
1288 | high_jump_standards
1289 | long_jump_pit
1290 | starting_blocks
1291 | relay_baton
1292 | ballpoint_pen
1293 | mechanical_pencil
1294 | correction_fluid
1295 | correction_tape
1296 | ruler
1297 | protractor
1298 | paper_clip
1299 | binder_clip
1300 | glue
1301 | double-sided_tape
1302 | memo_pad
1303 | file_folder
1304 | trumpet
1305 | oboe
1306 | bass_guitar
1307 | cello
1308 | viola
1309 | bassoon
1310 | french_horn
1311 | trombone
1312 | tuba
1313 | xylophone
1314 | glockenspiel
1315 | accordion
1316 | mandolin
1317 | dulcimer
1318 | organ
1319 | synthesizer
1320 | steel_drum
1321 | bongos
1322 | drum_set
1323 | game_console
1324 | smartwatch
1325 | scanner
1326 | external_hard_drive
1327 | flash_drive
1328 | soundbar
1329 | virtual_reality_headset
1330 | wireless_chargers
1331 | power_bank
1332 | e-reader
1333 | action_camera
1334 | gaming_chairs
1335 | electric_kettle
1336 | humidifier
1337 | dehumidifier
1338 | stereo_system
1339 | cd_player
1340 | dvd_player
1341 | bookshelf
1342 | power_drill
1343 | level_(tools)
1344 | chisel
1345 | sandpaper
1346 | wood_plane
1347 | drill_bit
1348 | power_saw
1349 | pickaxe
1350 | trowel
1351 | tweezers
1352 | hair_brush
1353 | lipstick
1354 | eye_liner
1355 | eye_shadow
1356 | nail_polish
1357 | fragrance
1358 | face_mask
1359 | go-kart
1360 | golf_cart
1361 | excavator
1362 | loader
1363 | steamroller
1364 | sedan
1365 | subway
1366 | maglev
1367 | cable_car
1368 | sailboat
1369 | rowboat
1370 | submarine
1371 | glider
1372 | hang_glider
1373 | fire_truck
1374 | police_car
1375 | hovercraft
1376 | crane
1377 | backhoe
1378 | bicycle_rack
1379 | speed_bump
1380 | fridge_magnet
1381 | sticker
1382 | candlestick
1383 | music_box
1384 | bookend
1385 | incense_burner
1386 | garland
1387 | selfie_stick
1388 | headlamp
1389 | beach_towel
1390 | inflatable_bed
1391 | travel_pillow
1392 | waist_pack
1393 | yo-yo
1394 | magic_cube
1395 | lego
1396 | building_blocks
1397 | stuffed_animal
1398 | puzzle
1399 | card_game
1400 | remote_control_car
1401 | protective_suit
1402 | test_tube
1403 | beaker
1404 | forceps
1405 | test_tube_holder
1406 | centrifuge
1407 | burette
1408 | petri_dish
1409 | balances
1410 | stirring_rod
1411 | whiteboard
1412 | chalk
1413 | closet
1414 | chewing_gum
1415 | popcorn
1416 | cheese_curls
1417 | raisins
1418 | applesauce
1419 | baboon
1420 | bait
1421 | bass_horn
1422 | batter_(food)
1423 | bedpan
1424 | beeper
1425 | Bible
1426 | birthday_card
1427 | pirate_flag
1428 | gameboard
1429 | bolo_tie
1430 | boom_microphone
1431 | brass_plaque
1432 | locker
1433 | candy_bar
1434 | elevator_car
1435 | car_battery
1436 | cargo_ship
1437 | carnation
1438 | casserole
1439 | chalice
1440 | checkbook
1441 | chime
1442 | cloak
1443 | cockroach
1444 | combination_lock
1445 | comic_book
1446 | sofa_bed
1447 | crowbar
1448 | dalmatian
1449 | die
1450 | tux
1451 | diving_board
1452 | dollar
1453 | drumstick
1454 | eclair
1455 | egg_roll
1456 | electric_chair
1457 | fedora
1458 | first-aid_kit
1459 | fishbowl
1460 | flash
1461 | funnel
1462 | garbage
1463 | generator
1464 | halter_top
1465 | hamper
1466 | hand_glass
1467 | hardback_book
1468 | hookah
1469 | hotplate
1470 | houseboat
1471 | ice_skate
1472 | joystick
1473 | kennel
1474 | keycard
1475 | lab_coat
1476 | lasagna
1477 | lightning_rod
1478 | machine_gun
1479 | mallard
1480 | mammoth
1481 | manatee
1482 | martini
1483 | masher
1484 | matchbox
1485 | milestone
1486 | milk_can
1487 | milkshake
1488 | mint_candy
1489 | music_stool
1490 | nosebag_(for_animals)
1491 | nutcracker
1492 | inkpad
1493 | pan_(metal_container)
1494 | pantyhose
1495 | paperback_book
1496 | paperweight
1497 | parchment
1498 | passenger_ship
1499 | pendulum
1500 | pennant
1501 | penny_(coin)
1502 | phonebook
1503 | piggy_bank
1504 | pin_(non_jewelry)
1505 | ping-pong_ball
1506 | pitchfork
1507 | playpen
1508 | plume
1509 | pool_table
1510 | prune
1511 | puppet
1512 | quesadilla
1513 | quiche
1514 | rib_(food)
1515 | scarecrow
1516 | shot_glass
1517 | shower_cap
1518 | sling_(bandage)
1519 | smoothie
1520 | sombrero
1521 | soya_milk
1522 | spear
1523 | crawfish
1524 | squid_(food)
1525 | steak_knife
1526 | syringe
1527 | tachometer
1528 | thimble
1529 | triangle_(musical_instrument)
1530 | truffle_(chocolate)
1531 | vat
1532 | vinegar
1533 | wardrobe
1534 | washbasin
1535 | water_heater
1536 | lighthouse
1537 | picnic_basket
1538 | barcode
1539 | balance_scale
1540 | cell_phone_charger
1541 | coffee_mug
1542 | shoehorn
1543 | zipper
1544 | pasta_strainer
1545 | swimwear
1546 | carabiner
1547 | swim_cap
1548 | camera_tripod
1549 | car_jack
1550 | pressure_cooker
1551 | bicycle_pump
1552 | sphygmomanometer
1553 | otoscope
1554 | stethoscope
1555 | defibrillator
1556 | infusion_pump
1557 | oxygen_concentrator
1558 | nebulizer
1559 | body_thermometer
1560 | leather_shoes
1561 | suv
1562 | high_heels
1563 | nightstand
1564 | tea_pot
1565 | gas_stove
1566 | computer_box
1567 | side_table
1568 | billards
1569 | paint_brush
1570 | american_football
1571 | swing
1572 | coffee_machine
1573 | ice_cream
1574 | golf_ball
1575 | hurdle
1576 | medal
1577 | megaphone
1578 | swan
1579 | rickshaw
1580 | recorder
1581 | board_eraser
1582 | meat_ball
1583 | rice_cooker
1584 | donkey
1585 | electric_drill
1586 | egg_tart
1587 | lighter
1588 | bun
1589 | target
1590 | spring_rolls
1591 | alcohol_lamp
1592 | fire_extinguisher
1593 | safety_hammer
1594 | steering_wheel
1595 | easel
1596 | drawing_board
1597 | palette
1598 | writing_brush
1599 | pigment
1600 | Inkstone
1601 | compass
1602 | canvas_bag
1603 | paper_bag
1604 | kitchen_paper
1605 | dumpling
1606 | Roast_Duck
1607 | ginger
1608 | chestnut
1609 | e-cigarette
1610 | cobblestone
1611 | life_buoy
1612 | darts
1613 | grill
1614 | massage_chair
1615 | cheongsam
1616 | electric_heater
1617 | urinal
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo.gif
--------------------------------------------------------------------------------
/demo_image/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_image/demo.jpg
--------------------------------------------------------------------------------
/demo_jntm.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_jntm.gif
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00000.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00000.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00001.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00001.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00002.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00002.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00003.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00003.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00004.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00004.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00005.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00005.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00006.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00006.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00007.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00007.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00008.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00008.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00009.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00009.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00010.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00010.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00011.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00011.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00012.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00012.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00013.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00013.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00014.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00014.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00015.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00015.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00016.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00016.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00017.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00017.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00018.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00018.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00019.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00019.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00020.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00020.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00021.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00021.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00022.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00022.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00023.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00023.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00024.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00025.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00025.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00026.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00026.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00027.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00027.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00028.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00028.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00029.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00029.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00030.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00030.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00031.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00031.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00032.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00032.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00033.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00033.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00034.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00034.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00035.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00035.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00036.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00036.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00037.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00037.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00038.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00038.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00039.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00039.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00040.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00040.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00041.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00041.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00042.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00042.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00043.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00043.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00044.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00044.jpg
--------------------------------------------------------------------------------
/demo_video/kunkun_00000_15_59/00045.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/demo_video/kunkun_00000_15_59/00045.jpg
--------------------------------------------------------------------------------
/mask_predictor.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List, Optional
2 |
3 | import cv2
4 | import numpy as np
5 | import torch
6 | from tqdm import tqdm
7 |
8 | from metaseg import SamAutomaticMaskGenerator, SamPredictor, sam_model_registry
9 | from metaseg.utils import download_model, load_image, load_video
10 |
11 |
12 | class SegAutoMaskPredictor:
13 | def __init__(self):
14 | self.model = None
15 | self.device = "cuda" if torch.cuda.is_available() else "cpu"
16 |
17 | def load_model(self, model_type):
18 | if self.model is None:
19 | self.model_path = download_model(model_type)
20 | self.model = sam_model_registry[model_type](checkpoint=self.model_path)
21 | self.model.to(device=self.device)
22 |
23 | return self.model
24 |
25 |
26 | def predict(self, frame, points_per_side, points_per_batch, min_area):
27 | frame = load_image(frame)
28 | mask_generator = SamAutomaticMaskGenerator(
29 | self.model, points_per_side=points_per_side, points_per_batch=points_per_batch, min_mask_region_area=min_area
30 | )
31 |
32 | masks = mask_generator.generate(frame)
33 |
34 | return frame, masks
35 |
36 | def save_image(self, source, model_type, points_per_side, points_per_batch, min_area):
37 | read_image = load_image(source)
38 | image, anns = self.predict(read_image, model_type, points_per_side, points_per_batch, min_area)
39 | if len(anns) == 0:
40 | return
41 |
42 | sorted_anns = sorted(anns, key=(lambda x: x["area"]), reverse=True)
43 | mask_image = np.zeros((anns[0]["segmentation"].shape[0], anns[0]["segmentation"].shape[1], 3), dtype=np.uint8)
44 | colors = np.random.randint(0, 255, size=(256, 3), dtype=np.uint8)
45 | for i, ann in enumerate(sorted_anns):
46 | m = ann["segmentation"]
47 | img = np.ones((m.shape[0], m.shape[1], 3), dtype=np.uint8)
48 | color = colors[i % 256]
49 | for i in range(3):
50 | img[:, :, 0] = color[0]
51 | img[:, :, 1] = color[1]
52 | img[:, :, 2] = color[2]
53 | img = cv2.bitwise_and(img, img, mask=m.astype(np.uint8))
54 | img = cv2.addWeighted(img, 0.35, np.zeros_like(img), 0.65, 0)
55 | mask_image = cv2.add(mask_image, img)
56 |
57 | combined_mask = cv2.add(image, mask_image)
58 | cv2.imwrite("output.jpg", combined_mask)
59 |
60 | return "output.jpg"
61 |
62 | def save_video(self, source, model_type, points_per_side, points_per_batch, min_area):
63 | cap, out = load_video(source)
64 | length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
65 | colors = np.random.randint(0, 255, size=(256, 3), dtype=np.uint8)
66 |
67 | for _ in tqdm(range(length)):
68 | ret, frame = cap.read()
69 | if not ret:
70 | break
71 |
72 | image, anns = self.predict(frame, model_type, points_per_side, points_per_batch, min_area)
73 | if len(anns) == 0:
74 | continue
75 |
76 | sorted_anns = sorted(anns, key=(lambda x: x["area"]), reverse=True)
77 | mask_image = np.zeros(
78 | (anns[0]["segmentation"].shape[0], anns[0]["segmentation"].shape[1], 3), dtype=np.uint8
79 | )
80 |
81 | for i, ann in enumerate(sorted_anns):
82 | m = ann["segmentation"]
83 | color = colors[i % 256] # Her nesne için farklı bir renk kullan
84 | img = np.zeros((m.shape[0], m.shape[1], 3), dtype=np.uint8)
85 | img[:, :, 0] = color[0]
86 | img[:, :, 1] = color[1]
87 | img[:, :, 2] = color[2]
88 | img = cv2.bitwise_and(img, img, mask=m.astype(np.uint8))
89 | img = cv2.addWeighted(img, 0.35, np.zeros_like(img), 0.65, 0)
90 | mask_image = cv2.add(mask_image, img)
91 |
92 | combined_mask = cv2.add(frame, mask_image)
93 | out.write(combined_mask)
94 |
95 | out.release()
96 | cap.release()
97 | cv2.destroyAllWindows()
98 |
99 | return "output.mp4"
100 |
101 |
102 | class SegManualMaskPredictor:
103 | def __init__(self):
104 | self.model = None
105 | self.device = "cuda" if torch.cuda.is_available() else "cpu"
106 |
107 | def load_model(self, model_type):
108 | if self.model is None:
109 | self.model_path = download_model(model_type)
110 | self.model = sam_model_registry[model_type](checkpoint=self.model_path)
111 | self.model.to(device=self.device)
112 |
113 | return self.model
114 |
115 | def load_mask(self, mask, random_color):
116 | if random_color:
117 | color = np.random.rand(3) * 255
118 | else:
119 | color = np.array([100, 50, 0])
120 |
121 | h, w = mask.shape[-2:]
122 | mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
123 | mask_image = mask_image.astype(np.uint8)
124 | return mask_image
125 |
126 | def load_box(self, box, image):
127 | x, y, w, h = int(box[0]), int(box[1]), int(box[2]), int(box[3])
128 | cv2.rectangle(image, (x, y), (w, h), (0, 255, 0), 2)
129 | return image
130 |
131 | def multi_boxes(self, boxes, predictor, image):
132 | input_boxes = torch.tensor(boxes, device=predictor.device)
133 | transformed_boxes = predictor.transform.apply_boxes_torch(input_boxes, image.shape[:2])
134 | return input_boxes, transformed_boxes
135 |
136 | def predict(
137 | self,
138 | frame,
139 | input_box=None,
140 | input_point=None,
141 | input_label=None,
142 | multimask_output=False,
143 | ):
144 | frame = load_image(frame)
145 | predictor = SamPredictor(self.model)
146 | predictor.set_image(frame)
147 |
148 | if type(input_box[0]) == list:
149 | input_boxes, new_boxes = self.multi_boxes(input_box, predictor, frame)
150 |
151 | masks, _, _ = predictor.predict_torch(
152 | point_coords=None,
153 | point_labels=None,
154 | boxes=new_boxes,
155 | multimask_output=False,
156 | )
157 |
158 | elif type(input_box[0]) == int:
159 | input_boxes = np.array(input_box)[None, :]
160 |
161 | masks, _, _ = predictor.predict(
162 | point_coords=input_point,
163 | point_labels=input_label,
164 | box=input_boxes,
165 | multimask_output=multimask_output,
166 | )
167 |
168 | return frame, masks, input_boxes
169 |
170 | def save_image(
171 | self,
172 | source,
173 | model_type,
174 | input_box=None,
175 | input_point=None,
176 | input_label=None,
177 | multimask_output=False,
178 | output_path="v0.jpg",
179 | ):
180 | read_image = load_image(source)
181 | image, anns, boxes = self.predict(read_image, model_type, input_box, input_point, input_label, multimask_output)
182 | if len(anns) == 0:
183 | return
184 |
185 | if type(input_box[0]) == list:
186 | for mask in anns:
187 | mask_image = self.load_mask(mask.cpu().numpy(), False)
188 |
189 | for box in boxes:
190 | image = self.load_box(box.cpu().numpy(), image)
191 |
192 | elif type(input_box[0]) == int:
193 | mask_image = self.load_mask(anns, True)
194 | image = self.load_box(input_box, image)
195 |
196 | combined_mask = cv2.add(image, mask_image)
197 | cv2.imwrite(output_path, combined_mask)
198 |
199 | return output_path
200 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs==1.4.4
2 | black==21.7b0
3 | charset-normalizer==3.1.0
4 | click==8.0.4
5 | coloredlogs==15.0.1
6 | contourpy==1.0.7
7 | cycler==0.11.0
8 | filelock==3.11.0
9 | flake8==3.9.2
10 | flatbuffers==23.3.3
11 | fonttools==4.39.3
12 | humanfriendly==10.0
13 | idna==3.4
14 | imgviz==1.7.2
15 | importlib-resources==5.12.0
16 | install==1.3.5
17 | isort==5.9.2
18 | Jinja2==3.1.2
19 | kiwisolver==1.4.4
20 | labelme==5.2.0
21 | MarkupSafe==2.1.2
22 | matplotlib==3.7.1
23 | mccabe==0.6.1
24 | metaseg==0.4.4
25 | mpmath==1.3.0
26 | mypy-extensions==1.0.0
27 | natsort==8.3.1
28 | networkx==3.1
29 | numpy==1.24.2
30 | onnxruntime==1.14.1
31 | opencv-python==4.7.0.72
32 | packaging==23.0
33 | pathspec==0.11.1
34 | Pillow==9.5.0
35 | protobuf==4.22.1
36 | pycocotools==2.0.6
37 | pycodestyle==2.7.0
38 | pyflakes==2.3.1
39 | pyparsing==3.0.9
40 | PyQt5==5.15.9
41 | PyQt5-Qt5==5.15.2
42 | PyQt5-sip==12.12.0
43 | python-dateutil==2.8.2
44 | PyYAML==6.0
45 | QtPy==2.3.1
46 | regex==2023.3.23
47 | requests==2.28.2
48 | six==1.16.0
49 | sympy==1.11.1
50 | termcolor==2.2.0
51 | tomli==1.2.3
52 | tqdm==4.65.0
53 | typing_extensions==4.5.0
54 | urllib3==1.26.15
55 | zipp==3.15.0
56 | git+https://github.com/facebookresearch/segment-anything.git
57 |
--------------------------------------------------------------------------------
/shape.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import math
3 |
4 | from qtpy import QtCore
5 | from qtpy import QtGui
6 |
7 | import labelme.utils
8 |
9 |
10 | # TODO(unknown):
11 | # - [opt] Store paths instead of creating new ones at each paint.
12 |
13 |
14 | DEFAULT_LINE_COLOR = QtGui.QColor(0, 255, 0, 128) # bf hovering
15 | DEFAULT_FILL_COLOR = QtGui.QColor(0, 255, 0, 128) # hovering
16 | DEFAULT_SELECT_LINE_COLOR = QtGui.QColor(255, 255, 255) # selected
17 | DEFAULT_SELECT_FILL_COLOR = QtGui.QColor(0, 255, 0, 155) # selected
18 | DEFAULT_VERTEX_FILL_COLOR = QtGui.QColor(0, 255, 0, 255) # hovering
19 | DEFAULT_HVERTEX_FILL_COLOR = QtGui.QColor(255, 255, 255, 255) # hovering
20 | DEFAULT_Negative_FILL_COLOR = QtGui.QColor(255, 0, 0) # hovering
21 |
22 |
23 | class Shape(object):
24 |
25 | # Render handles as squares
26 | P_SQUARE = 0
27 |
28 | # Render handles as circles
29 | P_ROUND = 1
30 |
31 | # Flag for the handles we would move if dragging
32 | MOVE_VERTEX = 0
33 |
34 | # Flag for all other handles on the curent shape
35 | NEAR_VERTEX = 1
36 |
37 | # The following class variables influence the drawing of all shape objects.
38 | line_color = DEFAULT_LINE_COLOR
39 | fill_color = DEFAULT_FILL_COLOR
40 | select_line_color = DEFAULT_SELECT_LINE_COLOR
41 | select_fill_color = DEFAULT_SELECT_FILL_COLOR
42 | vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR
43 | nvertex_fill_color = DEFAULT_Negative_FILL_COLOR
44 | hvertex_fill_color = DEFAULT_HVERTEX_FILL_COLOR
45 | point_type = P_ROUND
46 | point_size = 3
47 | scale = 1.0
48 |
49 | def __init__(
50 | self,
51 | label=None,
52 | line_color=None,
53 | shape_type=None,
54 | flags=None,
55 | group_id=None,
56 | ):
57 | self.label = label
58 | self.group_id = group_id
59 | self.points = []
60 | self.fill = False
61 | self.selected = False
62 | self.shape_type = shape_type
63 | self.flags = flags
64 | self.other_data = {}
65 |
66 | self._highlightIndex = None
67 | self._highlightMode = self.NEAR_VERTEX
68 | self._highlightSettings = {
69 | self.NEAR_VERTEX: (4, self.P_ROUND),
70 | self.MOVE_VERTEX: (1.5, self.P_SQUARE),
71 | }
72 |
73 | self._closed = False
74 |
75 | if line_color is not None:
76 | # Override the class line_color attribute
77 | # with an object attribute. Currently this
78 | # is used for drawing the pending line a different color.
79 | self.line_color = line_color
80 |
81 | self.shape_type = shape_type
82 |
83 | @property
84 | def shape_type(self):
85 | return self._shape_type
86 |
87 | @shape_type.setter
88 | def shape_type(self, value):
89 | if value is None:
90 | value = "polygon"
91 | if value not in [
92 | "polygon",
93 | "rectangle",
94 | "point",
95 | "line",
96 | "circle",
97 | "linestrip",
98 | ]:
99 | raise ValueError("Unexpected shape_type: {}".format(value))
100 | self._shape_type = value
101 |
102 | def close(self):
103 | self._closed = True
104 |
105 | def addPoint(self, point):
106 | if self.points and point == self.points[0]:
107 | self.close()
108 | else:
109 | self.points.append(point)
110 |
111 | def canAddPoint(self):
112 | return self.shape_type in ["polygon", "linestrip"]
113 |
114 | def popPoint(self):
115 | if self.points:
116 | return self.points.pop()
117 | return None
118 |
119 | def insertPoint(self, i, point):
120 | self.points.insert(i, point)
121 |
122 | def removePoint(self, i):
123 | self.points.pop(i)
124 |
125 | def isClosed(self):
126 | return self._closed
127 |
128 | def setOpen(self):
129 | self._closed = False
130 |
131 | def getRectFromLine(self, pt1, pt2):
132 | x1, y1 = pt1.x(), pt1.y()
133 | x2, y2 = pt2.x(), pt2.y()
134 | return QtCore.QRectF(x1, y1, x2 - x1, y2 - y1)
135 |
136 | def paint(self, painter, flag = 1, proposal_flag = 0):
137 | if self.points:
138 | color = (
139 | self.select_line_color if self.selected else self.line_color
140 | )
141 | if proposal_flag == 1:
142 | color = (
143 | QtGui.QColor(30, 30, 200)
144 | )
145 | pen = QtGui.QPen(color)
146 | npen = QtGui.QPen(self.nvertex_fill_color)
147 | if flag == 1:
148 | # Try using integer sizes for smoother drawing(?)
149 | pen.setWidth(max(1, int(round(2.0 / self.scale))))
150 | painter.setPen(pen)
151 | else:
152 | npen.setWidth(max(1, int(round(2.0 / self.scale))))
153 | painter.setPen(npen)
154 |
155 | line_path = QtGui.QPainterPath()
156 | vrtx_path = QtGui.QPainterPath()
157 | if self.shape_type == "rectangle":
158 | assert len(self.points) in [1, 2]
159 | if len(self.points) == 2:
160 | rectangle = self.getRectFromLine(*self.points)
161 | line_path.addRect(rectangle)
162 | for i in range(len(self.points)):
163 | self.drawVertex(vrtx_path, i)
164 | elif self.shape_type == "circle":
165 | assert len(self.points) in [1, 2]
166 | if len(self.points) == 2:
167 | rectangle = self.getCircleRectFromLine(self.points)
168 | line_path.addEllipse(rectangle)
169 | for i in range(len(self.points)):
170 | self.drawVertex(vrtx_path, i)
171 | elif self.shape_type == "linestrip":
172 | line_path.moveTo(self.points[0])
173 | for i, p in enumerate(self.points):
174 | line_path.lineTo(p)
175 | self.drawVertex(vrtx_path, i)
176 | elif self.shape_type == "point":
177 | for i, p in enumerate(self.points):
178 | self.drawVertex(vrtx_path, i)
179 | else:
180 | line_path.moveTo(self.points[0])
181 | # Uncommenting the following line will draw 2 paths
182 | # for the 1st vertex, and make it non-filled, which
183 | # may be desirable.
184 | # self.drawVertex(vrtx_path, 0)
185 |
186 | for i, p in enumerate(self.points):
187 | line_path.lineTo(p)
188 | self.drawVertex(vrtx_path, i)
189 | if self.isClosed():
190 | line_path.lineTo(self.points[0])
191 |
192 | painter.drawPath(line_path)
193 | if proposal_flag == 0:
194 | painter.drawPath(vrtx_path)
195 | painter.fillPath(vrtx_path, self._vertex_fill_color)
196 | if self.fill:
197 | color = (
198 | self.select_fill_color
199 | if self.selected
200 | else self.fill_color
201 | )
202 | if proposal_flag == 1:
203 | color = (
204 | QtGui.QColor(30, 30, 200,100)
205 | )
206 | painter.fillPath(line_path, color)
207 | if proposal_flag == 0:
208 | cx, cy = self.get_center_points(self.points)
209 | if self.group_id is not None:
210 | painter.drawText(int(cx), int(cy), self.label + ',' + str(self.group_id))
211 | else:
212 | painter.drawText(int(cx), int(cy), self.label)
213 |
214 | def get_center_points(self, points):
215 | xs = []
216 | ys = []
217 | for point in points:
218 | xs.append(point.x())
219 | ys.append(point.y())
220 |
221 | return sum(xs)/len(xs) - 15, max(ys) + 10
222 |
223 | def drawVertex(self, path, i):
224 | d = self.point_size / self.scale
225 | shape = self.point_type
226 | point = self.points[i]
227 | if i == self._highlightIndex:
228 | size, shape = self._highlightSettings[self._highlightMode]
229 | d *= size
230 | if self._highlightIndex is not None:
231 | self._vertex_fill_color = self.hvertex_fill_color
232 | else:
233 | self._vertex_fill_color = self.vertex_fill_color
234 | if shape == self.P_SQUARE:
235 | path.addRect(point.x() - d / 2, point.y() - d / 2, d, d)
236 | elif shape == self.P_ROUND:
237 | path.addEllipse(point, d / 2.0, d / 2.0)
238 | else:
239 | assert False, "unsupported vertex shape"
240 |
241 | def nearestVertex(self, point, epsilon):
242 | min_distance = float("inf")
243 | min_i = None
244 | for i, p in enumerate(self.points):
245 | dist = labelme.utils.distance(p - point)
246 | if dist <= epsilon and dist < min_distance:
247 | min_distance = dist
248 | min_i = i
249 | return min_i
250 |
251 | def nearestEdge(self, point, epsilon):
252 | min_distance = float("inf")
253 | post_i = None
254 | for i in range(len(self.points)):
255 | line = [self.points[i - 1], self.points[i]]
256 | dist = labelme.utils.distancetoline(point, line)
257 | if dist <= epsilon and dist < min_distance:
258 | min_distance = dist
259 | post_i = i
260 | return post_i
261 |
262 | def containsPoint(self, point):
263 | return self.makePath().contains(point)
264 |
265 | def getCircleRectFromLine(self, line):
266 | """Computes parameters to draw with `QPainterPath::addEllipse`"""
267 | if len(line) != 2:
268 | return None
269 | (c, point) = line
270 | r = line[0] - line[1]
271 | d = math.sqrt(math.pow(r.x(), 2) + math.pow(r.y(), 2))
272 | rectangle = QtCore.QRectF(c.x() - d, c.y() - d, 2 * d, 2 * d)
273 | return rectangle
274 |
275 | def makePath(self):
276 | if self.shape_type == "rectangle":
277 | path = QtGui.QPainterPath()
278 | if len(self.points) == 2:
279 | rectangle = self.getRectFromLine(*self.points)
280 | path.addRect(rectangle)
281 | elif self.shape_type == "circle":
282 | path = QtGui.QPainterPath()
283 | if len(self.points) == 2:
284 | rectangle = self.getCircleRectFromLine(self.points)
285 | path.addEllipse(rectangle)
286 | else:
287 | path = QtGui.QPainterPath(self.points[0])
288 | for p in self.points[1:]:
289 | path.lineTo(p)
290 | return path
291 |
292 | def boundingRect(self):
293 | return self.makePath().boundingRect()
294 |
295 | def moveBy(self, offset):
296 | self.points = [p + offset for p in self.points]
297 |
298 | def moveVertexBy(self, i, offset):
299 | self.points[i] = self.points[i] + offset
300 |
301 | def highlightVertex(self, i, action):
302 | """Highlight a vertex appropriately based on the current action
303 |
304 | Args:
305 | i (int): The vertex index
306 | action (int): The action
307 | (see Shape.NEAR_VERTEX and Shape.MOVE_VERTEX)
308 | """
309 | self._highlightIndex = i
310 | self._highlightMode = action
311 |
312 | def highlightClear(self):
313 | """Clear the highlighted point"""
314 | self._highlightIndex = None
315 |
316 | def copy(self):
317 | return copy.deepcopy(self)
318 |
319 | def __len__(self):
320 | return len(self.points)
321 |
322 | def __getitem__(self, key):
323 | return self.points[key]
324 |
325 | def __setitem__(self, key, value):
326 | self.points[key] = value
327 |
--------------------------------------------------------------------------------
/utils/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haochenheheda/segment-anything-annotator/584cbb87af4e153563609fa8003c6fefcee0c282/utils/.DS_Store
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 |
3 | from ._io import lblsave
4 |
5 | from .image import apply_exif_orientation
6 | from .image import img_arr_to_b64
7 | from .image import img_b64_to_arr
8 | from .image import img_data_to_arr
9 | from .image import img_data_to_pil
10 | from .image import img_data_to_png_data
11 | from .image import img_pil_to_data
12 |
13 | from .shape import labelme_shapes_to_label
14 | from .shape import masks_to_bboxes
15 | from .shape import polygons_to_mask
16 | from .shape import shape_to_mask
17 | from .shape import shapes_to_label
18 |
19 | from .qt import newIcon
20 | from .qt import newButton
21 | from .qt import newAction
22 | from .qt import addActions
23 | from .qt import labelValidator
24 | from .qt import struct
25 | from .qt import distance
26 | from .qt import distancetoline
27 | from .qt import fmtShortcut
28 |
--------------------------------------------------------------------------------
/utils/_io.py:
--------------------------------------------------------------------------------
1 | import os.path as osp
2 |
3 | import numpy as np
4 | import PIL.Image
5 |
6 |
7 | def lblsave(filename, lbl):
8 | import imgviz
9 |
10 | if osp.splitext(filename)[1] != ".png":
11 | filename += ".png"
12 | # Assume label ranses [-1, 254] for int32,
13 | # and [0, 255] for uint8 as VOC.
14 | if lbl.min() >= -1 and lbl.max() < 255:
15 | lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode="P")
16 | colormap = imgviz.label_colormap()
17 | lbl_pil.putpalette(colormap.flatten())
18 | lbl_pil.save(filename)
19 | else:
20 | raise ValueError(
21 | "[%s] Cannot save the pixel-wise class label as PNG. "
22 | "Please consider using the .npy format." % filename
23 | )
24 |
--------------------------------------------------------------------------------
/utils/download_model.py:
--------------------------------------------------------------------------------
1 | import os
2 | import urllib.request
3 |
4 |
5 | def download_model(model_type):
6 | """
7 | model_type: str, A string representing the model type. It can be 'vit_h', 'vit_l', or 'vit_b'.
8 | """
9 |
10 | # A dictionary containing model types as keys and their respective URLs as values
11 | model_urls = {
12 | "vit_h": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
13 | "vit_l": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
14 | "vit_b": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
15 | }
16 |
17 | # Check if the model file already exists and model_type is in model_urls
18 | filename = f"{model_type}.pth"
19 | if not os.path.exists(filename) and model_type in model_urls:
20 | url = model_urls[model_type]
21 | print(f"Downloading {model_type} model from {url}...")
22 | urllib.request.urlretrieve(url, filename)
23 | print(f"{model_type} model has been successfully downloaded and saved as '{filename}'.")
24 | elif os.path.exists(filename):
25 | print(f"{model_type} model already exists as '{filename}'. Skipping download.")
26 | else:
27 | raise ValueError("Invalid model type. It should be 'vit_h', 'vit_l', or 'vit_b'.")
28 |
29 | return filename
30 |
--------------------------------------------------------------------------------
/utils/image.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import io
3 |
4 | import numpy as np
5 | import PIL.ExifTags
6 | import PIL.Image
7 | import PIL.ImageOps
8 |
9 |
10 | def img_data_to_pil(img_data):
11 | f = io.BytesIO()
12 | f.write(img_data)
13 | img_pil = PIL.Image.open(f)
14 | return img_pil
15 |
16 |
17 | def img_data_to_arr(img_data):
18 | img_pil = img_data_to_pil(img_data)
19 | img_arr = np.array(img_pil)
20 | return img_arr
21 |
22 |
23 | def img_b64_to_arr(img_b64):
24 | img_data = base64.b64decode(img_b64)
25 | img_arr = img_data_to_arr(img_data)
26 | return img_arr
27 |
28 |
29 | def img_pil_to_data(img_pil):
30 | f = io.BytesIO()
31 | img_pil.save(f, format="PNG")
32 | img_data = f.getvalue()
33 | return img_data
34 |
35 |
36 | def img_arr_to_b64(img_arr):
37 | img_pil = PIL.Image.fromarray(img_arr)
38 | f = io.BytesIO()
39 | img_pil.save(f, format="PNG")
40 | img_bin = f.getvalue()
41 | if hasattr(base64, "encodebytes"):
42 | img_b64 = base64.encodebytes(img_bin)
43 | else:
44 | img_b64 = base64.encodestring(img_bin)
45 | return img_b64
46 |
47 |
48 | def img_data_to_png_data(img_data):
49 | with io.BytesIO() as f:
50 | f.write(img_data)
51 | img = PIL.Image.open(f)
52 |
53 | with io.BytesIO() as f:
54 | img.save(f, "PNG")
55 | f.seek(0)
56 | return f.read()
57 |
58 |
59 | def apply_exif_orientation(image):
60 | try:
61 | exif = image._getexif()
62 | except AttributeError:
63 | exif = None
64 |
65 | if exif is None:
66 | return image
67 |
68 | exif = {
69 | PIL.ExifTags.TAGS[k]: v
70 | for k, v in exif.items()
71 | if k in PIL.ExifTags.TAGS
72 | }
73 |
74 | orientation = exif.get("Orientation", None)
75 |
76 | if orientation == 1:
77 | # do nothing
78 | return image
79 | elif orientation == 2:
80 | # left-to-right mirror
81 | return PIL.ImageOps.mirror(image)
82 | elif orientation == 3:
83 | # rotate 180
84 | return image.transpose(PIL.Image.ROTATE_180)
85 | elif orientation == 4:
86 | # top-to-bottom mirror
87 | return PIL.ImageOps.flip(image)
88 | elif orientation == 5:
89 | # top-to-left mirror
90 | return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270))
91 | elif orientation == 6:
92 | # rotate 270
93 | return image.transpose(PIL.Image.ROTATE_270)
94 | elif orientation == 7:
95 | # top-to-right mirror
96 | return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90))
97 | elif orientation == 8:
98 | # rotate 90
99 | return image.transpose(PIL.Image.ROTATE_90)
100 | else:
101 | return image
102 |
--------------------------------------------------------------------------------
/utils/qt.py:
--------------------------------------------------------------------------------
1 | from math import sqrt
2 | import os.path as osp
3 |
4 | import numpy as np
5 |
6 | from qtpy import QtCore
7 | from qtpy import QtGui
8 | from qtpy import QtWidgets
9 |
10 |
11 | here = osp.dirname(osp.abspath(__file__))
12 |
13 |
14 | def newIcon(icon):
15 | icons_dir = osp.join(here, "../icons")
16 | return QtGui.QIcon(osp.join(":/", icons_dir, "%s.png" % icon))
17 |
18 |
19 | def newButton(text, icon=None, slot=None):
20 | b = QtWidgets.QPushButton(text)
21 | if icon is not None:
22 | b.setIcon(newIcon(icon))
23 | if slot is not None:
24 | b.clicked.connect(slot)
25 | return b
26 |
27 |
28 | def newAction(
29 | parent,
30 | text,
31 | slot=None,
32 | shortcut=None,
33 | icon=None,
34 | tip=None,
35 | checkable=False,
36 | enabled=True,
37 | checked=False,
38 | ):
39 | """Create a new action and assign callbacks, shortcuts, etc."""
40 | a = QtWidgets.QAction(text, parent)
41 | if icon is not None:
42 | a.setIconText(text.replace(" ", "\n"))
43 | a.setIcon(newIcon(icon))
44 | if shortcut is not None:
45 | if isinstance(shortcut, (list, tuple)):
46 | a.setShortcuts(shortcut)
47 | else:
48 | a.setShortcut(shortcut)
49 | if tip is not None:
50 | a.setToolTip(tip)
51 | a.setStatusTip(tip)
52 | if slot is not None:
53 | a.triggered.connect(slot)
54 | if checkable:
55 | a.setCheckable(True)
56 | a.setEnabled(enabled)
57 | a.setChecked(checked)
58 | return a
59 |
60 |
61 | def addActions(widget, actions):
62 | for action in actions:
63 | if action is None:
64 | widget.addSeparator()
65 | elif isinstance(action, QtWidgets.QMenu):
66 | widget.addMenu(action)
67 | else:
68 | widget.addAction(action)
69 |
70 |
71 | def labelValidator():
72 | return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None)
73 |
74 |
75 | class struct(object):
76 | def __init__(self, **kwargs):
77 | self.__dict__.update(kwargs)
78 |
79 |
80 | def distance(p):
81 | return sqrt(p.x() * p.x() + p.y() * p.y())
82 |
83 |
84 | def distancetoline(point, line):
85 | p1, p2 = line
86 | p1 = np.array([p1.x(), p1.y()])
87 | p2 = np.array([p2.x(), p2.y()])
88 | p3 = np.array([point.x(), point.y()])
89 | if np.dot((p3 - p1), (p2 - p1)) < 0:
90 | return np.linalg.norm(p3 - p1)
91 | if np.dot((p3 - p2), (p1 - p2)) < 0:
92 | return np.linalg.norm(p3 - p2)
93 | if np.linalg.norm(p2 - p1) == 0:
94 | return 0
95 | return np.linalg.norm(np.cross(p2 - p1, p1 - p3)) / np.linalg.norm(p2 - p1)
96 |
97 |
98 | def fmtShortcut(text):
99 | mod, key = text.split("+", 1)
100 | return "%s+%s" % (mod, key)
101 |
--------------------------------------------------------------------------------
/utils/shape.py:
--------------------------------------------------------------------------------
1 | import math
2 | import uuid
3 |
4 | import numpy as np
5 | import PIL.Image
6 | import PIL.ImageDraw
7 |
8 | from labelme.logger import logger
9 |
10 |
11 | def polygons_to_mask(img_shape, polygons, shape_type=None):
12 | logger.warning(
13 | "The 'polygons_to_mask' function is deprecated, "
14 | "use 'shape_to_mask' instead."
15 | )
16 | return shape_to_mask(img_shape, points=polygons, shape_type=shape_type)
17 |
18 |
19 | def shape_to_mask(
20 | img_shape, points, shape_type=None, line_width=10, point_size=5
21 | ):
22 | mask = np.zeros(img_shape[:2], dtype=np.uint8)
23 | mask = PIL.Image.fromarray(mask)
24 | draw = PIL.ImageDraw.Draw(mask)
25 | xy = [tuple(point) for point in points]
26 | if shape_type == "circle":
27 | assert len(xy) == 2, "Shape of shape_type=circle must have 2 points"
28 | (cx, cy), (px, py) = xy
29 | d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2)
30 | draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1)
31 | elif shape_type == "rectangle":
32 | assert len(xy) == 2, "Shape of shape_type=rectangle must have 2 points"
33 | draw.rectangle(xy, outline=1, fill=1)
34 | elif shape_type == "line":
35 | assert len(xy) == 2, "Shape of shape_type=line must have 2 points"
36 | draw.line(xy=xy, fill=1, width=line_width)
37 | elif shape_type == "linestrip":
38 | draw.line(xy=xy, fill=1, width=line_width)
39 | elif shape_type == "point":
40 | assert len(xy) == 1, "Shape of shape_type=point must have 1 points"
41 | cx, cy = xy[0]
42 | r = point_size
43 | draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1)
44 | else:
45 | assert len(xy) > 2, "Polygon must have points more than 2"
46 | draw.polygon(xy=xy, outline=1, fill=1)
47 | mask = np.array(mask, dtype=bool)
48 | return mask
49 |
50 |
51 | def shapes_to_label(img_shape, shapes, label_name_to_value):
52 | cls = np.zeros(img_shape[:2], dtype=np.int32)
53 | ins = np.zeros_like(cls)
54 | instances = []
55 | for shape in shapes:
56 | points = shape["points"]
57 | label = shape["label"]
58 | group_id = shape.get("group_id")
59 | if group_id is None:
60 | group_id = uuid.uuid1()
61 | shape_type = shape.get("shape_type", None)
62 |
63 | cls_name = label
64 | instance = (cls_name, group_id)
65 |
66 | if instance not in instances:
67 | instances.append(instance)
68 | ins_id = instances.index(instance) + 1
69 | cls_id = label_name_to_value[cls_name]
70 |
71 | mask = shape_to_mask(img_shape[:2], points, shape_type)
72 | cls[mask] = cls_id
73 | ins[mask] = ins_id
74 |
75 | return cls, ins
76 |
77 |
78 | def labelme_shapes_to_label(img_shape, shapes):
79 | logger.warn(
80 | "labelme_shapes_to_label is deprecated, so please use "
81 | "shapes_to_label."
82 | )
83 |
84 | label_name_to_value = {"_background_": 0}
85 | for shape in shapes:
86 | label_name = shape["label"]
87 | if label_name in label_name_to_value:
88 | label_value = label_name_to_value[label_name]
89 | else:
90 | label_value = len(label_name_to_value)
91 | label_name_to_value[label_name] = label_value
92 |
93 | lbl, _ = shapes_to_label(img_shape, shapes, label_name_to_value)
94 | return lbl, label_name_to_value
95 |
96 |
97 | def masks_to_bboxes(masks):
98 | if masks.ndim != 3:
99 | raise ValueError(
100 | "masks.ndim must be 3, but it is {}".format(masks.ndim)
101 | )
102 | if masks.dtype != bool:
103 | raise ValueError(
104 | "masks.dtype must be bool type, but it is {}".format(masks.dtype)
105 | )
106 | bboxes = []
107 | for mask in masks:
108 | where = np.argwhere(mask)
109 | (y1, x1), (y2, x2) = where.min(0), where.max(0) + 1
110 | bboxes.append((y1, x1, y2, x2))
111 | bboxes = np.asarray(bboxes, dtype=np.float32)
112 | return bboxes
113 |
--------------------------------------------------------------------------------