Lint (#50)
- Add continuous integration - Add Versioneer - Refactor of preprocessing - Add tables of evaluation
This commit is contained in:
parent
6299859d95
commit
934622bc81
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
monoloco/_version.py export-subst
|
||||
81
.github/workflows/tests.yml
vendored
Normal file
81
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
|
||||
# Adapted from https://github.com/openpifpaf/openpifpaf/blob/main/.github/workflows/test.yml,
|
||||
#which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
# and licensed under GNU AGPLv3
|
||||
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.7
|
||||
torch: 1.7.1+cpu
|
||||
torchvision: 0.8.2+cpu
|
||||
torch-source: https://download.pytorch.org/whl/torch_stable.html
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
torch: 1.7.1+cpu
|
||||
torchvision: 0.8.2+cpu
|
||||
torch-source: https://download.pytorch.org/whl/cpu/torch_stable.html
|
||||
- os: macos-latest
|
||||
python: 3.7
|
||||
torch: 1.7.1
|
||||
torchvision: 0.8.2
|
||||
torch-source: https://download.pytorch.org/whl/torch_stable.html
|
||||
- os: macos-latest
|
||||
python: 3.8
|
||||
torch: 1.7.1
|
||||
torchvision: 0.8.2
|
||||
torch-source: https://download.pytorch.org/whl/torch_stable.html
|
||||
- os: windows-latest
|
||||
python: 3.7
|
||||
torch: 1.7.1+cpu
|
||||
torchvision: 0.8.2+cpu
|
||||
torch-source: https://download.pytorch.org/whl/torch_stable.html
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
if: ${{ !matrix.conda }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Set up Conda
|
||||
if: matrix.conda
|
||||
uses: s-weigand/setup-conda@v1
|
||||
with:
|
||||
update-conda: true
|
||||
python-version: ${{ matrix.python }}
|
||||
conda-channels: anaconda, conda-forge
|
||||
- run: conda --version
|
||||
if: matrix.conda
|
||||
- run: which python
|
||||
if: matrix.conda
|
||||
- run: python --version
|
||||
- name: Install
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools
|
||||
python -m pip install -e ".[test]"
|
||||
- name: Print environment
|
||||
run: |
|
||||
python -m pip freeze
|
||||
python --version
|
||||
python -c "import monoloco; print(monoloco.__version__)"
|
||||
- name: Lint monoloco
|
||||
run: |
|
||||
pylint monoloco --disable=fixme
|
||||
- name: Lint tests
|
||||
if: matrix.os != 'windows-latest' # because of path separator
|
||||
run: |
|
||||
pylint tests/*.py --disable=fixme
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
pytest -vv
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
.idea/
|
||||
data/
|
||||
data
|
||||
.DS_store
|
||||
__pycache__
|
||||
monoloco/*.pyc
|
||||
|
||||
26
.pylintrc
26
.pylintrc
@ -1,26 +0,0 @@
|
||||
|
||||
|
||||
[BASIC]
|
||||
variable-rgx=[a-z0-9_]{1,30}$ # to accept 2 (dfferent) letters variables
|
||||
|
||||
|
||||
Good-names=xx,dd,zz,hh,ww,pp,kk,lr,w1,w2,w3,mm,im,uv,ax,COV_MIN,CONF_MIN
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
disable=import-error,invalid-name,unused-variable,E1102,missing-docstring,useless-object-inheritance,duplicate-code,too-many-arguments,too-many-instance-attributes,too-many-locals,too-few-public-methods,arguments-differ,logging-format-interpolation,import-outside-toplevel
|
||||
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
|
||||
generated-members=numpy.*,torch.*,cv2.*
|
||||
|
||||
ignored-modules=nuscenes, tabulate, cv2
|
||||
|
||||
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=120
|
||||
11
.travis.yml
11
.travis.yml
@ -1,11 +0,0 @@
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
install:
|
||||
- pip install --upgrade pip setuptools
|
||||
- pip install ".[test]"
|
||||
script:
|
||||
- pylint monoloco --disable=unused-variable,fixme
|
||||
- pytest -v
|
||||
13
LICENSE
13
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2018-2021 by EPFL/VITA. All rights reserved.
|
||||
Copyright 2018-2021 by Lorenzo Bertoni and contributors. All rights reserved.
|
||||
|
||||
This project and all its files are licensed under
|
||||
GNU AGPLv3 or later version.
|
||||
@ -7,3 +7,14 @@ If this license is not suitable for your business or project
|
||||
please contact EPFL-TTO (https://tto.epfl.ch/) for a full commercial license.
|
||||
|
||||
This software may not be used to harm any person deliberately or for any military application.
|
||||
|
||||
|
||||
The following files are based on the OpenPifPaf project which is
|
||||
"Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved." and licensed under GNU AGPLv3.
|
||||
|
||||
- tests/test_train_mono.py
|
||||
- tests/test_train_stereo.py
|
||||
- monoloco/visuals/pifpaf_show.py
|
||||
- monoloco/train/losses.py
|
||||
- monoloco/predict.py
|
||||
-.github/workflows/tests.yml
|
||||
|
||||
661
LICENSE.AGPL
661
LICENSE.AGPL
@ -1,661 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include versioneer.py
|
||||
include monoloco/_version.py
|
||||
60
README.md
60
README.md
@ -1,4 +1,7 @@
|
||||
# Monoloco library [](https://pepy.tech/project/monoloco)
|
||||
# Monoloco library [](https://pepy.tech/project/monoloco)
|
||||
Continuously tested on Linux, MacOS and Windows: [](https://github.com/vita-epfl/monoloco/actions?query=workflow%3ATests)
|
||||
|
||||
|
||||
|
||||
<img src="docs/monoloco.gif" alt="gif" />
|
||||
|
||||
@ -114,9 +117,9 @@ If you provide a ground-truth json file to compare the predictions of the networ
|
||||
|
||||
For an example image, run the following command:
|
||||
|
||||
```
|
||||
```sh
|
||||
python -m monoloco.run predict docs/002282.png \
|
||||
--path_gt <to match results with ground-truths> \
|
||||
--path_gt names-kitti-200615-1022.json \
|
||||
-o <output directory> \
|
||||
--long-edge <rescale the image by providing dimension of long side>
|
||||
--n_dropout <50 to include epistemic uncertainty, 0 otherwise>
|
||||
@ -129,7 +132,7 @@ To show all the instances estimated by MonoLoco add the argument `show_all` to t
|
||||

|
||||
|
||||
It is also possible to run [openpifpaf](https://github.com/vita-epfl/openpifpaf) directly
|
||||
by usingt `--mode keypoints`. All the other pifpaf arguments are also supported
|
||||
by using `--mode keypoints`. All the other pifpaf arguments are also supported
|
||||
and can be checked with `python -m monoloco.run predict --help`.
|
||||
|
||||

|
||||
@ -199,7 +202,7 @@ python -m monoloco.run train --joints data/arrays/joints-kitti-201202-1743.json
|
||||
|
||||
While for the MonStereo ones just change the input joints and add `--mode stereo`
|
||||
```
|
||||
python3 -m monoloco.run train --joints data/arrays/joints-kitti-201202-1022.json --mode stereo
|
||||
python3 -m monoloco.run train --lr 0.002 --joints data/arrays/joints-kitti-201202-1022.json --mode stereo
|
||||
```
|
||||
|
||||
If you are interested in the original results of the MonoLoco ICCV article (now improved with MonoLoco++), please refer to the tag v0.4.9 in this repository.
|
||||
@ -219,40 +222,58 @@ The code supports this option (by running the predict script and using `--mode p
|
||||
### Data structure
|
||||
|
||||
data
|
||||
├── outputs
|
||||
├── arrays
|
||||
├── models
|
||||
├── kitti
|
||||
├── logs
|
||||
├── output
|
||||
|
||||
Run the following inside monoloco repository:
|
||||
```
|
||||
mkdir data
|
||||
cd data
|
||||
mkdir arrays models kitti logs output
|
||||
mkdir outputs arrays kitti
|
||||
```
|
||||
|
||||
|
||||
### Kitti Dataset
|
||||
Annotations from a pose detector needs to be stored in a folder. With PifPaf:
|
||||
Download kitti images (from left and right cameras), ground-truth files (labels), and calibration files from their [website](http://www.cvlibs.net/datasets/kitti/eval_object.php?obj_benchmark=3d) and save them inside the `data` folder as shown below.
|
||||
|
||||
```
|
||||
data
|
||||
├── kitti
|
||||
├── gt
|
||||
├── calib
|
||||
├── images
|
||||
├── images_right
|
||||
|
||||
|
||||
The network takes as inputs 2D keypoints annotations. To create them run PifPaf over the saved images:
|
||||
```sh
|
||||
python -m openpifpaf.predict \
|
||||
--glob "<kitti images directory>/*.png" \
|
||||
--glob "data/kitti/images/*.png" \
|
||||
--json-output <directory to contain predictions> \
|
||||
--checkpoint=shufflenetv2k30 \
|
||||
--instance-threshold=0.05 --seed-threshold 0.05 --force-complete-pose
|
||||
```
|
||||
Once the step is complete, the below commands transform all the annotations into a single json file that will used for training.
|
||||
**Horizontal flipping**
|
||||
|
||||
To augment the dataset, we apply horizontal flipping on the detected poses. To include small variations in the pose, we use the poses from the right-camera (the dataset uses a stereo camera). As there are no labels for the right camera, the code automatically correct the ground truth depth by taking into account the camera baseline.
|
||||
To obtain these poses, run pifpaf also on the folder of right images. Make sure to save annotations into a different folder, and call the right folder: `<NameOfTheLeftFolder>_right`
|
||||
|
||||
**Recall**
|
||||
|
||||
To maximize the recall (at the cost of the computational time), it's possible to upscale the images with the command `--long_edge 2500` (\~scale 2).
|
||||
|
||||
Once this step is complete, the below commands transform all the annotations into a single json file that will used for training.
|
||||
|
||||
|
||||
|
||||
For MonoLoco++:
|
||||
```
|
||||
```sh
|
||||
python -m monoloco.run prep --dir_ann <directory that contains annotations>
|
||||
```
|
||||
|
||||
For MonStereo:
|
||||
```
|
||||
python -m monoloco.run prep --mode stereo --dir_ann <directory that contains annotations>
|
||||
```sh
|
||||
python -m monoloco.run prep --mode stereo --dir_ann <directory that contains left annotations>
|
||||
```
|
||||
|
||||
### Collective Activity Dataset
|
||||
@ -324,18 +345,19 @@ and save them into `data/kitti/monodepth`
|
||||
To include also geometric baselines and MonoLoco, download a monoloco model, save it in `data/models`, and add the flag ``--baselines`` to the evaluation command
|
||||
|
||||
|
||||
|
||||
The evaluation file will run the model over all the annotations and compare the results with KITTI ground-truth and the downloaded baselines. For this run:
|
||||
```
|
||||
python -m monoloco.run eval \
|
||||
--dir_ann <annotation directory> \
|
||||
--model <model path> \
|
||||
--model #TODO \
|
||||
--generate \
|
||||
--save \
|
||||
````
|
||||
For stereo results add `--mode stereo` and select `--model #TODO. Below, the resulting table of results and an example of the saved figures.
|
||||
|
||||
<img src="docs/results.jpg" width="700"/>
|
||||
|
||||
<img src="docs/results_stereo.jpg" width="550"/>
|
||||
<img src="docs/results_monstereo.jpg" width="700"/>
|
||||
|
||||
|
||||
### Relative Average Precision Localization: RALP-5% (MonStereo)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 295 KiB |
BIN
docs/results.jpg
Normal file
BIN
docs/results.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 385 KiB |
BIN
docs/results_monstereo.jpg
Normal file
BIN
docs/results_monstereo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 368 KiB |
@ -3,4 +3,6 @@
|
||||
Open implementation of MonoLoco / MonoLoco++ / MonStereo
|
||||
"""
|
||||
|
||||
__version__ = '0.5.0'
|
||||
from ._version import get_versions
|
||||
__version__ = get_versions()['version']
|
||||
del get_versions
|
||||
|
||||
527
monoloco/_version.py
Normal file
527
monoloco/_version.py
Normal file
@ -0,0 +1,527 @@
|
||||
|
||||
# This file helps to compute a version number in source trees obtained from
|
||||
# git-archive tarball (such as those provided by githubs download-from-tag
|
||||
# feature). Distribution tarballs (built by setup.py sdist) and build
|
||||
# directories (produced by setup.py build) will contain a much shorter file
|
||||
# that just contains the computed version number.
|
||||
|
||||
# This file is released into the public domain. Generated by
|
||||
# versioneer-0.19 (https://github.com/python-versioneer/python-versioneer)
|
||||
|
||||
# pylint: skip-file
|
||||
|
||||
"""Git implementation of _version.py."""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_keywords():
|
||||
"""Get the keywords needed to look up the version information."""
|
||||
# these strings will be replaced by git during git-archive.
|
||||
# setup.py/versioneer.py will grep for the variable names, so they must
|
||||
# each be defined on a line of their own. _version.py will just call
|
||||
# get_keywords().
|
||||
git_refnames = "$Format:%d$"
|
||||
git_full = "$Format:%H$"
|
||||
git_date = "$Format:%ci$"
|
||||
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
|
||||
return keywords
|
||||
|
||||
|
||||
class VersioneerConfig:
|
||||
"""Container for Versioneer configuration parameters."""
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Create, populate and return the VersioneerConfig() object."""
|
||||
# these strings are filled in when 'setup.py versioneer' creates
|
||||
# _version.py
|
||||
cfg = VersioneerConfig()
|
||||
cfg.VCS = "git"
|
||||
cfg.style = "pep440"
|
||||
cfg.tag_prefix = "v"
|
||||
cfg.parentdir_prefix = "None"
|
||||
cfg.versionfile_source = "monoloco/_version.py"
|
||||
cfg.verbose = False
|
||||
return cfg
|
||||
|
||||
|
||||
class NotThisMethod(Exception):
|
||||
"""Exception raised if a method is not valid for the current scenario."""
|
||||
|
||||
|
||||
LONG_VERSION_PY = {}
|
||||
HANDLERS = {}
|
||||
|
||||
|
||||
def register_vcs_handler(vcs, method): # decorator
|
||||
"""Create decorator to mark a method as the handler of a VCS."""
|
||||
def decorate(f):
|
||||
"""Store f in HANDLERS[vcs][method]."""
|
||||
if vcs not in HANDLERS:
|
||||
HANDLERS[vcs] = {}
|
||||
HANDLERS[vcs][method] = f
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
|
||||
env=None):
|
||||
"""Call the given command(s)."""
|
||||
assert isinstance(commands, list)
|
||||
p = None
|
||||
for c in commands:
|
||||
try:
|
||||
dispcmd = str([c] + args)
|
||||
# remember shell=False, so use git.cmd on windows, not just git
|
||||
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=(subprocess.PIPE if hide_stderr
|
||||
else None))
|
||||
break
|
||||
except EnvironmentError:
|
||||
e = sys.exc_info()[1]
|
||||
if e.errno == errno.ENOENT:
|
||||
continue
|
||||
if verbose:
|
||||
print("unable to run %s" % dispcmd)
|
||||
print(e)
|
||||
return None, None
|
||||
else:
|
||||
if verbose:
|
||||
print("unable to find command, tried %s" % (commands,))
|
||||
return None, None
|
||||
stdout = p.communicate()[0].strip().decode()
|
||||
if p.returncode != 0:
|
||||
if verbose:
|
||||
print("unable to run %s (error)" % dispcmd)
|
||||
print("stdout was %s" % stdout)
|
||||
return None, p.returncode
|
||||
return stdout, p.returncode
|
||||
|
||||
|
||||
def versions_from_parentdir(parentdir_prefix, root, verbose):
|
||||
"""Try to determine the version from the parent directory name.
|
||||
|
||||
Source tarballs conventionally unpack into a directory that includes both
|
||||
the project name and a version string. We will also support searching up
|
||||
two directory levels for an appropriately named parent directory
|
||||
"""
|
||||
rootdirs = []
|
||||
|
||||
for i in range(3):
|
||||
dirname = os.path.basename(root)
|
||||
if dirname.startswith(parentdir_prefix):
|
||||
return {"version": dirname[len(parentdir_prefix):],
|
||||
"full-revisionid": None,
|
||||
"dirty": False, "error": None, "date": None}
|
||||
else:
|
||||
rootdirs.append(root)
|
||||
root = os.path.dirname(root) # up a level
|
||||
|
||||
if verbose:
|
||||
print("Tried directories %s but none started with prefix %s" %
|
||||
(str(rootdirs), parentdir_prefix))
|
||||
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
|
||||
|
||||
|
||||
@register_vcs_handler("git", "get_keywords")
|
||||
def git_get_keywords(versionfile_abs):
|
||||
"""Extract version information from the given file."""
|
||||
# the code embedded in _version.py can just fetch the value of these
|
||||
# keywords. When used from setup.py, we don't want to import _version.py,
|
||||
# so we do it with a regexp instead. This function is not used from
|
||||
# _version.py.
|
||||
keywords = {}
|
||||
try:
|
||||
f = open(versionfile_abs, "r")
|
||||
for line in f.readlines():
|
||||
if line.strip().startswith("git_refnames ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["refnames"] = mo.group(1)
|
||||
if line.strip().startswith("git_full ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["full"] = mo.group(1)
|
||||
if line.strip().startswith("git_date ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["date"] = mo.group(1)
|
||||
f.close()
|
||||
except EnvironmentError:
|
||||
pass
|
||||
return keywords
|
||||
|
||||
|
||||
@register_vcs_handler("git", "keywords")
|
||||
def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
||||
"""Get version information from git keywords."""
|
||||
if not keywords:
|
||||
raise NotThisMethod("no keywords at all, weird")
|
||||
date = keywords.get("date")
|
||||
if date is not None:
|
||||
# Use only the last line. Previous lines may contain GPG signature
|
||||
# information.
|
||||
date = date.splitlines()[-1]
|
||||
|
||||
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
|
||||
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
|
||||
# -like" string, which we must then edit to make compliant), because
|
||||
# it's been around since git-1.5.3, and it's too difficult to
|
||||
# discover which version we're using, or to work around using an
|
||||
# older one.
|
||||
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||
refnames = keywords["refnames"].strip()
|
||||
if refnames.startswith("$Format"):
|
||||
if verbose:
|
||||
print("keywords are unexpanded, not using")
|
||||
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
|
||||
refs = set([r.strip() for r in refnames.strip("()").split(",")])
|
||||
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
|
||||
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
|
||||
TAG = "tag: "
|
||||
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
|
||||
if not tags:
|
||||
# Either we're using git < 1.8.3, or there really are no tags. We use
|
||||
# a heuristic: assume all version tags have a digit. The old git %d
|
||||
# expansion behaves like git log --decorate=short and strips out the
|
||||
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
|
||||
# between branches and tags. By ignoring refnames without digits, we
|
||||
# filter out many common branch names like "release" and
|
||||
# "stabilization", as well as "HEAD" and "master".
|
||||
tags = set([r for r in refs if re.search(r'\d', r)])
|
||||
if verbose:
|
||||
print("discarding '%s', no digits" % ",".join(refs - tags))
|
||||
if verbose:
|
||||
print("likely tags: %s" % ",".join(sorted(tags)))
|
||||
for ref in sorted(tags):
|
||||
# sorting will prefer e.g. "2.0" over "2.0rc1"
|
||||
if ref.startswith(tag_prefix):
|
||||
r = ref[len(tag_prefix):]
|
||||
if verbose:
|
||||
print("picking %s" % r)
|
||||
return {"version": r,
|
||||
"full-revisionid": keywords["full"].strip(),
|
||||
"dirty": False, "error": None,
|
||||
"date": date}
|
||||
# no suitable tags, so version is "0+unknown", but full hex is still there
|
||||
if verbose:
|
||||
print("no suitable tags, using unknown + full revision id")
|
||||
return {"version": "0+unknown",
|
||||
"full-revisionid": keywords["full"].strip(),
|
||||
"dirty": False, "error": "no suitable tags", "date": None}
|
||||
|
||||
|
||||
@register_vcs_handler("git", "pieces_from_vcs")
|
||||
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
||||
"""Get version from 'git describe' in the root of the source tree.
|
||||
|
||||
This only gets called if the git-archive 'subst' keywords were *not*
|
||||
expanded, and _version.py hasn't already been rewritten with a short
|
||||
version string, meaning we're inside a checked out source tree.
|
||||
"""
|
||||
GITS = ["git"]
|
||||
if sys.platform == "win32":
|
||||
GITS = ["git.cmd", "git.exe"]
|
||||
|
||||
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
|
||||
hide_stderr=True)
|
||||
if rc != 0:
|
||||
if verbose:
|
||||
print("Directory %s not under git control" % root)
|
||||
raise NotThisMethod("'git rev-parse --git-dir' returned error")
|
||||
|
||||
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
|
||||
# if there isn't one, this yields HEX[-dirty] (no NUM)
|
||||
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
|
||||
"--always", "--long",
|
||||
"--match", "%s*" % tag_prefix],
|
||||
cwd=root)
|
||||
# --long was added in git-1.5.5
|
||||
if describe_out is None:
|
||||
raise NotThisMethod("'git describe' failed")
|
||||
describe_out = describe_out.strip()
|
||||
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
|
||||
if full_out is None:
|
||||
raise NotThisMethod("'git rev-parse' failed")
|
||||
full_out = full_out.strip()
|
||||
|
||||
pieces = {}
|
||||
pieces["long"] = full_out
|
||||
pieces["short"] = full_out[:7] # maybe improved later
|
||||
pieces["error"] = None
|
||||
|
||||
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
|
||||
# TAG might have hyphens.
|
||||
git_describe = describe_out
|
||||
|
||||
# look for -dirty suffix
|
||||
dirty = git_describe.endswith("-dirty")
|
||||
pieces["dirty"] = dirty
|
||||
if dirty:
|
||||
git_describe = git_describe[:git_describe.rindex("-dirty")]
|
||||
|
||||
# now we have TAG-NUM-gHEX or HEX
|
||||
|
||||
if "-" in git_describe:
|
||||
# TAG-NUM-gHEX
|
||||
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
|
||||
if not mo:
|
||||
# unparseable. Maybe git-describe is misbehaving?
|
||||
pieces["error"] = ("unable to parse git-describe output: '%s'"
|
||||
% describe_out)
|
||||
return pieces
|
||||
|
||||
# tag
|
||||
full_tag = mo.group(1)
|
||||
if not full_tag.startswith(tag_prefix):
|
||||
if verbose:
|
||||
fmt = "tag '%s' doesn't start with prefix '%s'"
|
||||
print(fmt % (full_tag, tag_prefix))
|
||||
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
|
||||
% (full_tag, tag_prefix))
|
||||
return pieces
|
||||
pieces["closest-tag"] = full_tag[len(tag_prefix):]
|
||||
|
||||
# distance: number of commits since tag
|
||||
pieces["distance"] = int(mo.group(2))
|
||||
|
||||
# commit: short hex revision ID
|
||||
pieces["short"] = mo.group(3)
|
||||
|
||||
else:
|
||||
# HEX: no tags
|
||||
pieces["closest-tag"] = None
|
||||
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
|
||||
cwd=root)
|
||||
pieces["distance"] = int(count_out) # total number of commits
|
||||
|
||||
# commit date: see ISO-8601 comment in git_versions_from_keywords()
|
||||
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
|
||||
cwd=root)[0].strip()
|
||||
# Use only the last line. Previous lines may contain GPG signature
|
||||
# information.
|
||||
date = date.splitlines()[-1]
|
||||
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||
|
||||
return pieces
|
||||
|
||||
|
||||
def plus_or_dot(pieces):
|
||||
"""Return a + if we don't already have one, else return a ."""
|
||||
if "+" in pieces.get("closest-tag", ""):
|
||||
return "."
|
||||
return "+"
|
||||
|
||||
|
||||
def render_pep440(pieces):
|
||||
"""Build up version string, with post-release "local version identifier".
|
||||
|
||||
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
|
||||
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
|
||||
|
||||
Exceptions:
|
||||
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
|
||||
pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_pre(pieces):
|
||||
"""TAG[.post0.devDISTANCE] -- No -dirty.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.post0.devDISTANCE
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"]:
|
||||
rendered += ".post0.dev%d" % pieces["distance"]
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post0.dev%d" % pieces["distance"]
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_post(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]+gHEX] .
|
||||
|
||||
The ".dev0" means dirty. Note that .dev0 sorts backwards
|
||||
(a dirty tree will appear "older" than the corresponding clean one),
|
||||
but you shouldn't be releasing software with -dirty anyways.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += ".post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "g%s" % pieces["short"]
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
rendered += "+g%s" % pieces["short"]
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_old(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]] .
|
||||
|
||||
The ".dev0" means dirty.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += ".post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_git_describe(pieces):
|
||||
"""TAG[-DISTANCE-gHEX][-dirty].
|
||||
|
||||
Like 'git describe --tags --dirty --always'.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"]:
|
||||
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||
else:
|
||||
# exception #1
|
||||
rendered = pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += "-dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_git_describe_long(pieces):
|
||||
"""TAG-DISTANCE-gHEX[-dirty].
|
||||
|
||||
Like 'git describe --tags --dirty --always -long'.
|
||||
The distance/hash is unconditional.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||
else:
|
||||
# exception #1
|
||||
rendered = pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += "-dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render(pieces, style):
|
||||
"""Render the given version pieces into the requested style."""
|
||||
if pieces["error"]:
|
||||
return {"version": "unknown",
|
||||
"full-revisionid": pieces.get("long"),
|
||||
"dirty": None,
|
||||
"error": pieces["error"],
|
||||
"date": None}
|
||||
|
||||
if not style or style == "default":
|
||||
style = "pep440" # the default
|
||||
|
||||
if style == "pep440":
|
||||
rendered = render_pep440(pieces)
|
||||
elif style == "pep440-pre":
|
||||
rendered = render_pep440_pre(pieces)
|
||||
elif style == "pep440-post":
|
||||
rendered = render_pep440_post(pieces)
|
||||
elif style == "pep440-old":
|
||||
rendered = render_pep440_old(pieces)
|
||||
elif style == "git-describe":
|
||||
rendered = render_git_describe(pieces)
|
||||
elif style == "git-describe-long":
|
||||
rendered = render_git_describe_long(pieces)
|
||||
else:
|
||||
raise ValueError("unknown style '%s'" % style)
|
||||
|
||||
return {"version": rendered, "full-revisionid": pieces["long"],
|
||||
"dirty": pieces["dirty"], "error": None,
|
||||
"date": pieces.get("date")}
|
||||
|
||||
|
||||
def get_versions():
|
||||
"""Get version information or return default if unable to do so."""
|
||||
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
|
||||
# __file__, we can work backwards from there to the root. Some
|
||||
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
|
||||
# case we can only use expanded keywords.
|
||||
|
||||
cfg = get_config()
|
||||
verbose = cfg.verbose
|
||||
|
||||
try:
|
||||
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
|
||||
verbose)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
try:
|
||||
root = os.path.realpath(__file__)
|
||||
# versionfile_source is the relative path from the top of the source
|
||||
# tree (where the .git directory might live) to this file. Invert
|
||||
# this to find the root from __file__.
|
||||
for i in cfg.versionfile_source.split('/'):
|
||||
root = os.path.dirname(root)
|
||||
except NameError:
|
||||
return {"version": "0+unknown", "full-revisionid": None,
|
||||
"dirty": None,
|
||||
"error": "unable to find root of source tree",
|
||||
"date": None}
|
||||
|
||||
try:
|
||||
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
|
||||
return render(pieces, cfg.style)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
try:
|
||||
if cfg.parentdir_prefix:
|
||||
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
return {"version": "0+unknown", "full-revisionid": None,
|
||||
"dirty": None,
|
||||
"error": "unable to compute version", "date": None}
|
||||
@ -123,7 +123,7 @@ def show_social(args, image_t, output_path, annotations, dic_out):
|
||||
|
||||
# Draw keypoints and orientation
|
||||
if 'front' in args.output_types:
|
||||
keypoint_sets, scores = get_pifpaf_outputs(annotations)
|
||||
keypoint_sets, _ = get_pifpaf_outputs(annotations)
|
||||
uv_centers = dic_out['uv_heads']
|
||||
sizes = [abs(dic_out['uv_heads'][idx][1] - uv_s[1]) / 1.5 for idx, uv_s in
|
||||
enumerate(dic_out['uv_shoulders'])]
|
||||
|
||||
@ -2,17 +2,23 @@
|
||||
import os
|
||||
import glob
|
||||
import csv
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from sklearn.metrics import accuracy_score
|
||||
try:
|
||||
from sklearn.metrics import accuracy_score
|
||||
ACCURACY_SCORE = copy.copy(accuracy_score)
|
||||
except ImportError:
|
||||
ACCURACY_SCORE = None
|
||||
|
||||
from monoloco.network import Loco
|
||||
from monoloco.network.process import factory_for_gt, preprocess_pifpaf
|
||||
from monoloco.activity import social_interactions
|
||||
from monoloco.utils import open_annotations, get_iou_matches, get_difficulty
|
||||
from ..prep import factory_file
|
||||
from ..network import Loco
|
||||
from ..network.process import factory_for_gt, preprocess_pifpaf
|
||||
from ..activity import social_interactions
|
||||
from ..utils import open_annotations, get_iou_matches, get_difficulty
|
||||
|
||||
|
||||
class ActivityEvaluator:
|
||||
@ -79,7 +85,7 @@ class ActivityEvaluator:
|
||||
im_size = image.size
|
||||
assert len(im_size) > 1, "image with frame0001 not available"
|
||||
|
||||
for idx, im_path in enumerate(images):
|
||||
for im_path in images:
|
||||
|
||||
# Collect PifPaf files and calibration
|
||||
basename = os.path.basename(im_path)
|
||||
@ -101,14 +107,12 @@ class ActivityEvaluator:
|
||||
self.estimate_activity(dic_out, matches, ys_gt, categories=categories)
|
||||
|
||||
# Print Results
|
||||
acc = accuracy_score(self.all_gt[seq], self.all_pred[seq])
|
||||
acc = ACCURACY_SCORE(self.all_gt[seq], self.all_pred[seq])
|
||||
print(f"Accuracy of category {seq}: {100*acc:.2f}%")
|
||||
cout_results(self.cnt, self.all_gt, self.all_pred, categories=self.sequences)
|
||||
|
||||
def eval_kitti(self):
|
||||
"""Parse KITTI Dataset and predict if people are talking or not"""
|
||||
|
||||
from ..utils import factory_file
|
||||
files = glob.glob(self.dir_data + '/*.txt')
|
||||
# files = [self.dir_gt_kitti + '/001782.txt']
|
||||
assert files, "Empty directory"
|
||||
@ -118,7 +122,7 @@ class ActivityEvaluator:
|
||||
# Collect PifPaf files and calibration
|
||||
basename, _ = os.path.splitext(os.path.basename(file))
|
||||
path_calib = os.path.join(self.dir_kk, basename + '.txt')
|
||||
annotations, kk, tt = factory_file(path_calib, self.dir_ann, basename)
|
||||
annotations, kk, _ = factory_file(path_calib, self.dir_ann, basename)
|
||||
|
||||
# Collect corresponding gt files (ys_gt: 1 or 0)
|
||||
path_gt = os.path.join(self.dir_data, basename + '.txt')
|
||||
@ -149,7 +153,7 @@ class ActivityEvaluator:
|
||||
self.cnt['gt'][key] += 1
|
||||
self.cnt['gt']['all'] += 1
|
||||
|
||||
for i_m, (idx, idx_gt) in enumerate(matches):
|
||||
for (idx, idx_gt) in matches:
|
||||
|
||||
# Select keys to update results for Collective or KITTI
|
||||
keys = ('all', categories[idx_gt])
|
||||
@ -184,7 +188,7 @@ def parse_gt_collective(dir_data, seq, path_pif):
|
||||
with open(path, "r") as ff:
|
||||
reader = csv.reader(ff, delimiter='\t')
|
||||
dic_frames = defaultdict(lambda: defaultdict(list))
|
||||
for idx, line in enumerate(reader):
|
||||
for line in reader:
|
||||
box = convert_box(line[1:5])
|
||||
cat = convert_category(line[5])
|
||||
dic_frames[line[0]]['boxes'].append(box)
|
||||
|
||||
@ -7,13 +7,20 @@ Evaluate MonStereo code on KITTI dataset using ALE metric
|
||||
import os
|
||||
import math
|
||||
import logging
|
||||
import copy
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from tabulate import tabulate
|
||||
import numpy as np
|
||||
try:
|
||||
import tabulate
|
||||
TABULATE = copy.copy(tabulate.tabulate)
|
||||
except ImportError:
|
||||
TABULATE = None
|
||||
|
||||
from ..utils import get_iou_matches, get_task_error, get_pixel_error, check_conditions, \
|
||||
get_difficulty, split_training, parse_ground_truth, get_iou_matches_matrix, average, find_cluster
|
||||
get_difficulty, split_training, get_iou_matches_matrix, average, find_cluster
|
||||
from ..prep import parse_ground_truth
|
||||
from ..visuals import show_results, show_spread, show_task_error, show_box_plot
|
||||
|
||||
|
||||
@ -29,7 +36,7 @@ class EvalKitti:
|
||||
METHODS_STEREO = ['3dop', 'psf', 'pseudo-lidar', 'e2e', 'oc-stereo']
|
||||
BASELINES = ['task_error', 'pixel_error']
|
||||
HEADERS = ('method', '<0.5', '<1m', '<2m', 'easy', 'moderate', 'hard', 'all')
|
||||
CATEGORIES = ('pedestrian',)
|
||||
CATEGORIES = ('pedestrian',) # extendable with person_sitting and/or cyclists
|
||||
methods = OUR_METHODS + METHODS_MONO + METHODS_STEREO
|
||||
|
||||
# Set directories
|
||||
@ -39,8 +46,7 @@ class EvalKitti:
|
||||
path_val = os.path.join('splits', 'kitti_val.txt')
|
||||
dir_logs = os.path.join('data', 'logs')
|
||||
assert os.path.exists(dir_logs), "No directory to save final statistics"
|
||||
dir_fig = os.path.join('data', 'figures')
|
||||
assert os.path.exists(dir_logs), "No directory to save figures"
|
||||
dir_fig = os.path.join('figures', 'results')
|
||||
|
||||
# Set thresholds to obtain comparable recalls
|
||||
thresh_iou_monoloco = 0.3
|
||||
@ -49,9 +55,10 @@ class EvalKitti:
|
||||
thresh_conf_base = 0.5
|
||||
|
||||
def __init__(self, args):
|
||||
|
||||
self.mode = args.mode
|
||||
assert self.mode in ('mono', 'stereo'), "mode not recognized"
|
||||
self.net = 'monstereo' if self.mode == 'stereo' else 'monoloco_pp'
|
||||
self.verbose = args.verbose
|
||||
self.net = args.net
|
||||
self.save = args.save
|
||||
self.show = args.show
|
||||
|
||||
@ -110,7 +117,7 @@ class EvalKitti:
|
||||
methods_out = defaultdict(tuple) # Save all methods for comparison
|
||||
|
||||
# Count ground_truth:
|
||||
boxes_gt, ys, truncs_gt, occs_gt = out_gt # pylint: disable=unbalanced-tuple-unpacking
|
||||
boxes_gt, _, truncs_gt, occs_gt, _ = out_gt # pylint: disable=unbalanced-tuple-unpacking
|
||||
for idx, box in enumerate(boxes_gt):
|
||||
mode = get_difficulty(box, truncs_gt[idx], occs_gt[idx])
|
||||
self.cnt_gt[mode] += 1
|
||||
@ -144,10 +151,13 @@ class EvalKitti:
|
||||
self.show_statistics()
|
||||
|
||||
def printer(self):
|
||||
if self.save:
|
||||
os.makedirs(self.dir_fig, exist_ok=True)
|
||||
if self.save or self.show:
|
||||
print('-' * 100)
|
||||
show_results(self.dic_stats, self.CLUSTERS, self.net, self.dir_fig, show=self.show, save=self.save)
|
||||
show_spread(self.dic_stats, self.CLUSTERS, self.net, self.dir_fig, show=self.show, save=self.save)
|
||||
if self.net == 'monstero':
|
||||
if self.net == 'monstereo':
|
||||
show_box_plot(self.errors, self.CLUSTERS, self.dir_fig, show=self.show, save=self.save)
|
||||
else:
|
||||
show_task_error(self.dir_fig, show=self.show, save=self.save)
|
||||
@ -201,7 +211,7 @@ class EvalKitti:
|
||||
def _estimate_error(self, out_gt, out, method):
|
||||
"""Estimate localization error"""
|
||||
|
||||
boxes_gt, ys, truncs_gt, occs_gt = out_gt
|
||||
boxes_gt, ys, truncs_gt, occs_gt, _ = out_gt
|
||||
|
||||
if method in self.OUR_METHODS:
|
||||
boxes, dds, cat, bis, epis = out
|
||||
@ -363,7 +373,7 @@ class EvalKitti:
|
||||
for key in all_methods]
|
||||
|
||||
results = [[key] + alp[idx] + ale[idx] for idx, key in enumerate(all_methods)]
|
||||
print(tabulate(results, headers=self.HEADERS))
|
||||
print(TABULATE(results, headers=self.HEADERS))
|
||||
print('-' * 90 + '\n')
|
||||
|
||||
def stats_height(self):
|
||||
@ -373,10 +383,8 @@ class EvalKitti:
|
||||
self.name = name
|
||||
# Iterate over each line of the gt file and save box location and distances
|
||||
out_gt = parse_ground_truth(path_gt, 'pedestrian')
|
||||
boxes_gt, ys, truncs_gt, occs_gt = out_gt # pylint: disable=unbalanced-tuple-unpacking
|
||||
for label in ys:
|
||||
for label in out_gt[1]:
|
||||
heights.append(label[4])
|
||||
import numpy as np
|
||||
tail1, tail2 = np.nanpercentile(np.array(heights), [5, 95])
|
||||
print(average(heights))
|
||||
print(len(heights))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# pylint: disable=too-many-statements,too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
"""Joints Analysis: Supplementary material of MonStereo"""
|
||||
|
||||
@ -11,26 +11,7 @@ import matplotlib.pyplot as plt
|
||||
|
||||
from ..utils import find_cluster, average
|
||||
from ..visuals.figures import get_distances
|
||||
|
||||
COCO_KEYPOINTS = [
|
||||
'nose', # 0
|
||||
'left_eye', # 1
|
||||
'right_eye', # 2
|
||||
'left_ear', # 3
|
||||
'right_ear', # 4
|
||||
'left_shoulder', # 5
|
||||
'right_shoulder', # 6
|
||||
'left_elbow', # 7
|
||||
'right_elbow', # 8
|
||||
'left_wrist', # 9
|
||||
'right_wrist', # 10
|
||||
'left_hip', # 11
|
||||
'right_hip', # 12
|
||||
'left_knee', # 13
|
||||
'right_knee', # 14
|
||||
'left_ankle', # 15
|
||||
'right_ankle', # 16
|
||||
]
|
||||
from ..prep.transforms import COCO_KEYPOINTS
|
||||
|
||||
|
||||
def joints_variance(joints, clusters, dic_ms):
|
||||
@ -184,8 +165,8 @@ def variance_figures(dic_fin, clusters):
|
||||
plt.title("Standard deviation of joints disparity")
|
||||
yys_p = [el for _, el in dic_fin['pifpaf']['std_d'].items()]
|
||||
yys_m = [el for _, el in dic_fin['mask']['std_d'].items()]
|
||||
yys_p_z = [el for _, el in dic_fin['pifpaf']['std_z'].items()]
|
||||
yys_m_z = [el for _, el in dic_fin['mask']['std_z'].items()]
|
||||
# yys_p_z = [el for _, el in dic_fin['pifpaf']['std_z'].items()]
|
||||
# yys_m_z = [el for _, el in dic_fin['mask']['std_z'].items()]
|
||||
plt.plot(xxs, yys_p, marker='s', label="PifPaf")
|
||||
plt.plot(xxs, yys_m, marker='o', label="Mask R-CNN")
|
||||
# plt.plot(xxs, yys_p_z, marker='s', color='b', label="PifPaf (meters)")
|
||||
|
||||
@ -13,10 +13,11 @@ import torch
|
||||
|
||||
from ..network import Loco
|
||||
from ..network.process import preprocess_pifpaf
|
||||
from ..network.geom_baseline import geometric_coordinates
|
||||
from ..utils import get_keypoints, pixel_to_camera, factory_file, factory_basename, make_new_directory, get_category, \
|
||||
from .geom_baseline import geometric_coordinates
|
||||
from ..utils import get_keypoints, pixel_to_camera, factory_basename, make_new_directory, get_category, \
|
||||
xyz_from_distance, read_and_rewrite
|
||||
from .stereo_baselines import baselines_association
|
||||
from ..prep import factory_file
|
||||
from .reid_baseline import get_reid_features, ReID
|
||||
|
||||
|
||||
@ -93,7 +94,7 @@ class GenerateKitti:
|
||||
make_new_directory(di)
|
||||
dir_out = {self.net: di}
|
||||
|
||||
for mode, names in self.baselines.items():
|
||||
for _, names in self.baselines.items():
|
||||
for name in names:
|
||||
di = os.path.join('data', 'kitti', name)
|
||||
make_new_directory(di)
|
||||
@ -106,7 +107,7 @@ class GenerateKitti:
|
||||
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(1242, 374))
|
||||
cat = get_category(keypoints, os.path.join(self.dir_byc, basename + '.json'))
|
||||
if keypoints:
|
||||
annotations_r, _, _ = factory_file(path_calib, self.dir_ann, basename, mode='right')
|
||||
annotations_r, _, _ = factory_file(path_calib, self.dir_ann, basename, ann_type='right')
|
||||
_, keypoints_r = preprocess_pifpaf(annotations_r, im_size=(1242, 374))
|
||||
|
||||
if self.net == 'monstereo':
|
||||
@ -121,7 +122,7 @@ class GenerateKitti:
|
||||
# Save txt files
|
||||
params = [kk, tt]
|
||||
path_txt = os.path.join(dir_out[self.net], basename + '.txt')
|
||||
save_txts(path_txt, boxes, all_outputs[self.net], params, mode=self.net, cat=cat)
|
||||
save_txts(path_txt, boxes, all_outputs[self.net], params, net=self.net, cat=cat)
|
||||
cnt_ann += len(boxes)
|
||||
cnt_file += 1
|
||||
|
||||
@ -136,7 +137,7 @@ class GenerateKitti:
|
||||
# monocular baselines
|
||||
for key in self.baselines['mono']:
|
||||
path_txt = {key: os.path.join(dir_out[key], basename + '.txt')}
|
||||
save_txts(path_txt[key], boxes, all_outputs[key], params, mode=key, cat=cat)
|
||||
save_txts(path_txt[key], boxes, all_outputs[key], params, net=key, cat=cat)
|
||||
|
||||
# stereo baselines
|
||||
if self.baselines['stereo']:
|
||||
@ -149,7 +150,7 @@ class GenerateKitti:
|
||||
|
||||
path_txt[key] = os.path.join(dir_out[key], basename + '.txt')
|
||||
save_txts(path_txt[key], all_inputs[key], all_outputs[key], params,
|
||||
mode='baseline',
|
||||
net='baseline',
|
||||
cat=cat)
|
||||
|
||||
print("\nSaved in {} txt {} annotations. Not found {} images".format(cnt_file, cnt_ann, cnt_no_file))
|
||||
@ -166,9 +167,9 @@ class GenerateKitti:
|
||||
|
||||
def _run_stereo_baselines(self, basename, boxes, keypoints, zzs, path_calib):
|
||||
|
||||
annotations_r, _, _ = factory_file(path_calib, self.dir_ann, basename, mode='right')
|
||||
annotations_r, _, _ = factory_file(path_calib, self.dir_ann, basename, ann_type='right')
|
||||
boxes_r, keypoints_r = preprocess_pifpaf(annotations_r, im_size=(1242, 374))
|
||||
_, kk, tt = factory_file(path_calib, self.dir_ann, basename)
|
||||
_, kk, _ = factory_file(path_calib, self.dir_ann, basename)
|
||||
|
||||
uv_centers = get_keypoints(keypoints, mode='bottom') # Kitti uses the bottom center to calculate depth
|
||||
xy_centers = pixel_to_camera(uv_centers, kk, 1)
|
||||
@ -198,15 +199,15 @@ class GenerateKitti:
|
||||
return dic_xyz
|
||||
|
||||
|
||||
def save_txts(path_txt, all_inputs, all_outputs, all_params, mode='monoloco', cat=None):
|
||||
def save_txts(path_txt, all_inputs, all_outputs, all_params, net='monoloco', cat=None):
|
||||
|
||||
assert mode in ('monoloco', 'monstereo', 'geometric', 'baseline', 'monoloco_pp')
|
||||
assert net in ('monoloco', 'monstereo', 'geometric', 'baseline', 'monoloco_pp')
|
||||
|
||||
if mode in ('monstereo', 'monoloco_pp'):
|
||||
if net in ('monstereo', 'monoloco_pp'):
|
||||
xyzd, bis, epis, yaws, hs, ws, ls = all_outputs[:]
|
||||
xyz = xyzd[:, 0:3]
|
||||
tt = [0, 0, 0]
|
||||
elif mode in ('monoloco', 'geometric'):
|
||||
elif net in ('monoloco', 'geometric'):
|
||||
tt = [0, 0, 0]
|
||||
dds, bis, epis, zzs_geom, xy_centers = all_outputs[:]
|
||||
xyz = xyz_from_distance(dds, xy_centers)
|
||||
@ -223,25 +224,18 @@ def save_txts(path_txt, all_inputs, all_outputs, all_params, mode='monoloco', ca
|
||||
yy = float(xyz[idx][1]) - tt[1]
|
||||
zz = float(xyz[idx][2]) - tt[2]
|
||||
|
||||
if mode == 'geometric':
|
||||
if net == 'geometric':
|
||||
zz = zzs_geom[idx]
|
||||
|
||||
cam_0 = [xx, yy, zz]
|
||||
bi = float(bis[idx])
|
||||
epi = float(epis[idx])
|
||||
if mode in ('monstereo', 'monoloco_pp'):
|
||||
if net in ('monstereo', 'monoloco_pp'):
|
||||
alpha, ry = float(yaws[0][idx]), float(yaws[1][idx])
|
||||
hwl = [float(hs[idx]), float(ws[idx]), float(ls[idx])]
|
||||
conf_scale = 0.035 # scale to obtain (approximately) same recall at evaluation
|
||||
else:
|
||||
alpha, ry, hwl = -10., -10., [0, 0, 0]
|
||||
|
||||
# Set the scale to obtain (approximately) same recall at evaluation
|
||||
if mode == 'monstereo':
|
||||
conf_scale = 0.03
|
||||
elif mode == 'monoloco_pp':
|
||||
conf_scale = 0.033
|
||||
# conf_scale = 0.035 # nuScenes for having same recall
|
||||
else:
|
||||
conf_scale = 0.05
|
||||
conf = conf_scale * (uv_box[-1]) / (bi / math.sqrt(xx ** 2 + yy ** 2 + zz ** 2))
|
||||
|
||||
|
||||
@ -1,17 +1,34 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..utils import pixel_to_camera, get_keypoints
|
||||
from monoloco.utils import pixel_to_camera, get_keypoints
|
||||
|
||||
AVERAGE_Y = 0.48
|
||||
CLUSTERS = ['10', '20', '30', 'all']
|
||||
|
||||
|
||||
def geometric_coordinates(keypoints, kk, average_y=0.48):
|
||||
""" Evaluate geometric depths for a set of keypoints"""
|
||||
|
||||
zzs_geom = []
|
||||
uv_shoulders = get_keypoints(keypoints, mode='shoulder')
|
||||
uv_hips = get_keypoints(keypoints, mode='hip')
|
||||
uv_centers = get_keypoints(keypoints, mode='center')
|
||||
|
||||
xy_shoulders = pixel_to_camera(uv_shoulders, kk, 1)
|
||||
xy_hips = pixel_to_camera(uv_hips, kk, 1)
|
||||
xy_centers = pixel_to_camera(uv_centers, kk, 1)
|
||||
|
||||
for idx, xy_shoulder in enumerate(xy_shoulders):
|
||||
zz = compute_depth(xy_shoulder, xy_hips[idx], average_y)
|
||||
zzs_geom.append(zz)
|
||||
return zzs_geom, xy_centers
|
||||
|
||||
|
||||
def geometric_baseline(joints):
|
||||
"""
|
||||
List of json files --> 2 lists with mean and std for each segment and the total count of instances
|
||||
@ -28,8 +45,6 @@ def geometric_baseline(joints):
|
||||
'right_ankle']
|
||||
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
cnt_tot = 0
|
||||
dic_dist = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
@ -48,13 +63,13 @@ def geometric_baseline(joints):
|
||||
errors = calculate_error(dic_dist['error'])
|
||||
|
||||
# Show results
|
||||
logger.info("Computed distance of {} annotations".format(cnt_tot))
|
||||
print("Computed distance of {} annotations".format(cnt_tot))
|
||||
for key in dic_h_means:
|
||||
logger.info("Average height of segment {} is {:.2f} with a std of {:.2f}".
|
||||
format(key, dic_h_means[key], dic_h_stds[key]))
|
||||
print("Average height of segment {} is {:.2f} with a std of {:.2f}".
|
||||
format(key, dic_h_means[key], dic_h_stds[key]))
|
||||
for clst in CLUSTERS:
|
||||
logger.info("Average error over the val set for clst {}: {:.2f}".format(clst, errors[clst]))
|
||||
logger.info("Joints used: {}".format(joints))
|
||||
print("Average error over the val set for clst {}: {:.2f}".format(clst, errors[clst]))
|
||||
print("Joints used: {}".format(joints))
|
||||
|
||||
|
||||
def update_distances(dic_fin, dic_dist, phase, average_y):
|
||||
@ -78,9 +93,9 @@ def update_distances(dic_fin, dic_dist, phase, average_y):
|
||||
dy_met = abs(float((dic_xyz['hip'][0][1] - dic_xyz['shoulder'][0][1])))
|
||||
|
||||
# Estimate distance for a single annotation
|
||||
z_met_real = compute_distance(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y,
|
||||
mode='real', dy_met=dy_met)
|
||||
z_met_approx = compute_distance(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y, mode='average')
|
||||
z_met_real = compute_depth(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y,
|
||||
mode='real', dy_met=dy_met)
|
||||
z_met_approx = compute_depth(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y, mode='average')
|
||||
|
||||
# Compute distance with respect to the center of the 3D bounding box
|
||||
d_real = math.sqrt(z_met_real ** 2 + dic_fin['boxes_3d'][idx][0] ** 2 + dic_fin['boxes_3d'][idx][1] ** 2)
|
||||
@ -94,9 +109,9 @@ def update_distances(dic_fin, dic_dist, phase, average_y):
|
||||
return cnt
|
||||
|
||||
|
||||
def compute_distance(xyz_norm_1, xyz_norm_2, average_y, mode='average', dy_met=0):
|
||||
def compute_depth(xyz_norm_1, xyz_norm_2, average_y, mode='average', dy_met=0):
|
||||
"""
|
||||
Compute distance Z of a mask annotation (solving a linear system) for 2 possible cases:
|
||||
Compute depth Z of a mask annotation (solving a linear system) for 2 possible cases:
|
||||
1. knowing specific height of the annotation (head-ankle) dy_met
|
||||
2. using mean height of people (average_y)
|
||||
"""
|
||||
|
||||
@ -27,7 +27,7 @@ def get_reid_features(reid_net, boxes, boxes_r, path_image, path_image_r):
|
||||
return features.cpu(), features_r.cpu()
|
||||
|
||||
|
||||
class ReID(object):
|
||||
class ReID:
|
||||
def __init__(self, weights_path, device, num_classes=751, height=256, width=128):
|
||||
super().__init__()
|
||||
torch.manual_seed(1)
|
||||
|
||||
@ -22,7 +22,7 @@ def baselines_association(baselines, zzs, keypoints, keypoints_right, reid_featu
|
||||
cnt_stereo['max'] = min(keypoints.shape[0], keypoints_r.shape[0]) # pylint: disable=E1136
|
||||
|
||||
# Filter joints disparity and calculate avg disparity
|
||||
avg_disparities, disparities_x, disparities_y = mask_joint_disparity(keypoints, keypoints_r)
|
||||
avg_disparities, _, _ = mask_joint_disparity(keypoints, keypoints_r)
|
||||
|
||||
# Iterate over each left pose
|
||||
for key in baselines:
|
||||
|
||||
@ -3,7 +3,7 @@ import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class MonStereoModel(nn.Module):
|
||||
class LocoModel(nn.Module):
|
||||
|
||||
def __init__(self, input_size, output_size=2, linear_size=512, p_dropout=0.2, num_stage=3, device='cuda'):
|
||||
super().__init__()
|
||||
|
||||
@ -1,213 +0,0 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from monoloco.utils import pixel_to_camera, get_keypoints
|
||||
|
||||
AVERAGE_Y = 0.48
|
||||
CLUSTERS = ['10', '20', '30', 'all']
|
||||
|
||||
|
||||
def geometric_coordinates(keypoints, kk, average_y=0.48):
|
||||
""" Evaluate geometric depths for a set of keypoints"""
|
||||
|
||||
zzs_geom = []
|
||||
uv_shoulders = get_keypoints(keypoints, mode='shoulder')
|
||||
uv_hips = get_keypoints(keypoints, mode='hip')
|
||||
uv_centers = get_keypoints(keypoints, mode='center')
|
||||
|
||||
xy_shoulders = pixel_to_camera(uv_shoulders, kk, 1)
|
||||
xy_hips = pixel_to_camera(uv_hips, kk, 1)
|
||||
xy_centers = pixel_to_camera(uv_centers, kk, 1)
|
||||
|
||||
for idx, xy_shoulder in enumerate(xy_shoulders):
|
||||
zz = compute_depth(xy_shoulders[idx], xy_hips[idx], average_y)
|
||||
zzs_geom.append(zz)
|
||||
return zzs_geom, xy_centers
|
||||
|
||||
|
||||
def geometric_baseline(joints):
|
||||
"""
|
||||
List of json files --> 2 lists with mean and std for each segment and the total count of instances
|
||||
|
||||
For each annotation:
|
||||
1. From gt boxes calculate the height (deltaY) for the segments head, shoulder, hip, ankle
|
||||
2. From mask boxes calculate distance of people using average height of people and real pixel height
|
||||
|
||||
For left-right ambiguities we chose always the average of the joints
|
||||
|
||||
The joints are mapped from 0 to 16 in the following order:
|
||||
['nose', 'left_eye', 'right_eye', 'left_ear', 'right_ear', 'left_shoulder', 'right_shoulder', 'left_elbow',
|
||||
'right_elbow', 'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee', 'right_knee', 'left_ankle',
|
||||
'right_ankle']
|
||||
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
cnt_tot = 0
|
||||
dic_dist = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
# Access the joints file
|
||||
with open(joints, 'r') as ff:
|
||||
dic_joints = json.load(ff)
|
||||
|
||||
# Calculate distances for all the instances in the joints dictionary
|
||||
for phase in ['train', 'val']:
|
||||
cnt = update_distances(dic_joints[phase], dic_dist, phase, AVERAGE_Y)
|
||||
cnt_tot += cnt
|
||||
|
||||
# Calculate mean and std of each segment
|
||||
dic_h_means = calculate_heights(dic_dist['heights'], mode='mean')
|
||||
dic_h_stds = calculate_heights(dic_dist['heights'], mode='std')
|
||||
errors = calculate_error(dic_dist['error'])
|
||||
|
||||
# Show results
|
||||
logger.info("Computed distance of {} annotations".format(cnt_tot))
|
||||
for key in dic_h_means:
|
||||
logger.info("Average height of segment {} is {:.2f} with a std of {:.2f}".
|
||||
format(key, dic_h_means[key], dic_h_stds[key]))
|
||||
for clst in CLUSTERS:
|
||||
logger.info("Average error over the val set for clst {}: {:.2f}".format(clst, errors[clst]))
|
||||
logger.info("Joints used: {}".format(joints))
|
||||
|
||||
|
||||
def update_distances(dic_fin, dic_dist, phase, average_y):
|
||||
|
||||
# Loop over each annotation in the json file corresponding to the image
|
||||
cnt = 0
|
||||
for idx, kps in enumerate(dic_fin['kps']):
|
||||
|
||||
# Extract pixel coordinates of head, shoulder, hip, ankle and and save them
|
||||
dic_uv = {mode: get_keypoints(kps, mode) for mode in ['head', 'shoulder', 'hip', 'ankle']}
|
||||
|
||||
# Convert segments from pixel coordinate to camera coordinate
|
||||
kk = dic_fin['K'][idx]
|
||||
z_met = dic_fin['boxes_3d'][idx][2]
|
||||
|
||||
# Create a dict with all annotations in meters
|
||||
dic_xyz = {key: pixel_to_camera(dic_uv[key], kk, z_met) for key in dic_uv}
|
||||
dic_xyz_norm = {key: pixel_to_camera(dic_uv[key], kk, 1) for key in dic_uv}
|
||||
|
||||
# Compute real height
|
||||
dy_met = abs(float((dic_xyz['hip'][0][1] - dic_xyz['shoulder'][0][1])))
|
||||
|
||||
# Estimate distance for a single annotation
|
||||
z_met_real = compute_depth(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y,
|
||||
mode='real', dy_met=dy_met)
|
||||
z_met_approx = compute_depth(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y, mode='average')
|
||||
|
||||
# Compute distance with respect to the center of the 3D bounding box
|
||||
d_real = math.sqrt(z_met_real ** 2 + dic_fin['boxes_3d'][idx][0] ** 2 + dic_fin['boxes_3d'][idx][1] ** 2)
|
||||
d_approx = math.sqrt(z_met_approx ** 2 +
|
||||
dic_fin['boxes_3d'][idx][0] ** 2 + dic_fin['boxes_3d'][idx][1] ** 2)
|
||||
|
||||
# Update the dictionary with distance and heights metrics
|
||||
dic_dist = update_dic_dist(dic_dist, dic_xyz, d_real, d_approx, phase)
|
||||
cnt += 1
|
||||
|
||||
return cnt
|
||||
|
||||
|
||||
def compute_depth(xyz_norm_1, xyz_norm_2, average_y, mode='average', dy_met=0):
|
||||
"""
|
||||
Compute depth Z of a mask annotation (solving a linear system) for 2 possible cases:
|
||||
1. knowing specific height of the annotation (head-ankle) dy_met
|
||||
2. using mean height of people (average_y)
|
||||
"""
|
||||
assert mode in ('average', 'real')
|
||||
|
||||
x1 = float(xyz_norm_1[0])
|
||||
y1 = float(xyz_norm_1[1])
|
||||
x2 = float(xyz_norm_2[0])
|
||||
y2 = float(xyz_norm_2[1])
|
||||
xx = (x1 + x2) / 2
|
||||
|
||||
# Choose if solving for provided height or average one.
|
||||
if mode == 'average':
|
||||
cc = - average_y # Y axis goes down
|
||||
else:
|
||||
cc = -dy_met
|
||||
|
||||
# Solving the linear system Ax = b
|
||||
matrix = np.array([[y1, 0, -xx],
|
||||
[0, -y1, 1],
|
||||
[y2, 0, -xx],
|
||||
[0, -y2, 1]])
|
||||
|
||||
bb = np.array([cc * xx, -cc, 0, 0]).reshape(4, 1)
|
||||
xx = np.linalg.lstsq(matrix, bb, rcond=None)
|
||||
z_met = abs(np.float(xx[0][1])) # Abs take into account specularity behind the observer
|
||||
|
||||
return z_met
|
||||
|
||||
|
||||
def update_dic_dist(dic_dist, dic_xyz, d_real, d_approx, phase):
|
||||
""" For every annotation in a single image, update the final dictionary"""
|
||||
|
||||
# Update the dict with heights metric
|
||||
if phase == 'train':
|
||||
dic_dist['heights']['head'].append(float(dic_xyz['head'][0][1]))
|
||||
dic_dist['heights']['shoulder'].append(float(dic_xyz['shoulder'][0][1]))
|
||||
dic_dist['heights']['hip'].append(float(dic_xyz['hip'][0][1]))
|
||||
dic_dist['heights']['ankle'].append(float(dic_xyz['ankle'][0][1]))
|
||||
|
||||
# Update the dict with distance metrics for the test phase
|
||||
if phase == 'val':
|
||||
error = abs(d_real - d_approx)
|
||||
|
||||
if d_real <= 10:
|
||||
dic_dist['error']['10'].append(error)
|
||||
elif d_real <= 20:
|
||||
dic_dist['error']['20'].append(error)
|
||||
elif d_real <= 30:
|
||||
dic_dist['error']['30'].append(error)
|
||||
else:
|
||||
dic_dist['error']['>30'].append(error)
|
||||
|
||||
dic_dist['error']['all'].append(error)
|
||||
|
||||
return dic_dist
|
||||
|
||||
|
||||
def calculate_heights(heights, mode):
|
||||
"""
|
||||
Compute statistics of heights based on the distance
|
||||
"""
|
||||
|
||||
assert mode in ('mean', 'std', 'max')
|
||||
heights_fin = {}
|
||||
|
||||
head_shoulder = np.array(heights['shoulder']) - np.array(heights['head'])
|
||||
shoulder_hip = np.array(heights['hip']) - np.array(heights['shoulder'])
|
||||
hip_ankle = np.array(heights['ankle']) - np.array(heights['hip'])
|
||||
|
||||
if mode == 'mean':
|
||||
heights_fin['head_shoulder'] = np.float(np.mean(head_shoulder)) * 100
|
||||
heights_fin['shoulder_hip'] = np.float(np.mean(shoulder_hip)) * 100
|
||||
heights_fin['hip_ankle'] = np.float(np.mean(hip_ankle)) * 100
|
||||
|
||||
elif mode == 'std':
|
||||
heights_fin['head_shoulder'] = np.float(np.std(head_shoulder)) * 100
|
||||
heights_fin['shoulder_hip'] = np.float(np.std(shoulder_hip)) * 100
|
||||
heights_fin['hip_ankle'] = np.float(np.std(hip_ankle)) * 100
|
||||
|
||||
elif mode == 'max':
|
||||
heights_fin['head_shoulder'] = np.float(np.max(head_shoulder)) * 100
|
||||
heights_fin['shoulder_hip'] = np.float(np.max(shoulder_hip)) * 100
|
||||
heights_fin['hip_ankle'] = np.float(np.max(hip_ankle)) * 100
|
||||
|
||||
return heights_fin
|
||||
|
||||
|
||||
def calculate_error(dic_errors):
|
||||
"""
|
||||
Compute statistics of distances based on the distance
|
||||
"""
|
||||
errors = {}
|
||||
for clst in dic_errors:
|
||||
errors[clst] = np.float(np.mean(np.array(dic_errors[clst])))
|
||||
return errors
|
||||
@ -1,141 +0,0 @@
|
||||
|
||||
import math
|
||||
import torch
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class CustomL1Loss(torch.nn.Module):
|
||||
"""
|
||||
L1 loss with more weight to errors at a shorter distance
|
||||
It inherits from nn.module so it supports backward
|
||||
"""
|
||||
|
||||
def __init__(self, dic_norm, device, beta=1):
|
||||
super().__init__()
|
||||
|
||||
self.dic_norm = dic_norm
|
||||
self.device = device
|
||||
self.beta = beta
|
||||
|
||||
@staticmethod
|
||||
def compute_weights(xx, beta=1):
|
||||
"""
|
||||
Return the appropriate weight depending on the distance and the hyperparameter chosen
|
||||
alpha = 1 refers to the curve of A Photogrammetric Approach for Real-time...
|
||||
It is made for unnormalized outputs (to be more understandable)
|
||||
From 70 meters on every value is weighted the same (0.1**beta)
|
||||
Alpha is optional value from Focal loss. Yet to be analyzed
|
||||
"""
|
||||
# alpha = np.maximum(1, 10 ** (beta - 1))
|
||||
alpha = 1
|
||||
ww = np.maximum(0.1, 1 - xx / 78)**beta
|
||||
|
||||
return alpha * ww
|
||||
|
||||
def print_loss(self):
|
||||
xx = np.linspace(0, 80, 100)
|
||||
y1 = self.compute_weights(xx, beta=1)
|
||||
y2 = self.compute_weights(xx, beta=2)
|
||||
y3 = self.compute_weights(xx, beta=3)
|
||||
plt.plot(xx, y1)
|
||||
plt.plot(xx, y2)
|
||||
plt.plot(xx, y3)
|
||||
plt.xlabel("Distance [m]")
|
||||
plt.ylabel("Loss function Weight")
|
||||
plt.legend(("Beta = 1", "Beta = 2", "Beta = 3"))
|
||||
plt.show()
|
||||
|
||||
def forward(self, output, target):
|
||||
|
||||
unnormalized_output = output.cpu().detach().numpy() * self.dic_norm['std']['Y'] + self.dic_norm['mean']['Y']
|
||||
weights_np = self.compute_weights(unnormalized_output, self.beta)
|
||||
weights = torch.from_numpy(weights_np).float().to(self.device) # To make weights in the same cuda device
|
||||
losses = torch.abs(output - target) * weights
|
||||
loss = losses.mean() # Mean over the batch
|
||||
return loss
|
||||
|
||||
|
||||
class LaplacianLoss(torch.nn.Module):
|
||||
"""1D Gaussian with std depending on the absolute distance
|
||||
"""
|
||||
def __init__(self, size_average=True, reduce=True, evaluate=False):
|
||||
super().__init__()
|
||||
self.size_average = size_average
|
||||
self.reduce = reduce
|
||||
self.evaluate = evaluate
|
||||
|
||||
def laplacian_1d(self, mu_si, xx):
|
||||
"""
|
||||
1D Gaussian Loss. f(x | mu, sigma). The network outputs mu and sigma. X is the ground truth distance.
|
||||
This supports backward().
|
||||
Inspired by
|
||||
https://github.com/naba89/RNN-Handwriting-Generation-Pytorch/blob/master/loss_functions.py
|
||||
|
||||
"""
|
||||
mu, si = mu_si[:, 0:1], mu_si[:, 1:2]
|
||||
# norm = xx - mu
|
||||
norm = 1 - mu / xx # Relative
|
||||
|
||||
term_a = torch.abs(norm) * torch.exp(-si)
|
||||
term_b = si
|
||||
norm_bi = (np.mean(np.abs(norm.cpu().detach().numpy())), np.mean(torch.exp(si).cpu().detach().numpy()))
|
||||
|
||||
if self.evaluate:
|
||||
return norm_bi
|
||||
return term_a + term_b
|
||||
|
||||
def forward(self, outputs, targets):
|
||||
|
||||
values = self.laplacian_1d(outputs, targets)
|
||||
|
||||
if not self.reduce or self.evaluate:
|
||||
return values
|
||||
if self.size_average:
|
||||
mean_values = torch.mean(values)
|
||||
return mean_values
|
||||
return torch.sum(values)
|
||||
|
||||
|
||||
class GaussianLoss(torch.nn.Module):
|
||||
"""1D Gaussian with std depending on the absolute distance
|
||||
"""
|
||||
def __init__(self, device, size_average=True, reduce=True, evaluate=False):
|
||||
super().__init__()
|
||||
self.size_average = size_average
|
||||
self.reduce = reduce
|
||||
self.evaluate = evaluate
|
||||
self.device = device
|
||||
|
||||
def gaussian_1d(self, mu_si, xx):
|
||||
"""
|
||||
1D Gaussian Loss. f(x | mu, sigma). The network outputs mu and sigma. X is the ground truth distance.
|
||||
This supports backward().
|
||||
Inspired by
|
||||
https://github.com/naba89/RNN-Handwriting-Generation-Pytorch/blob/master/loss_functions.py
|
||||
"""
|
||||
mu, si = mu_si[:, 0:1], mu_si[:, 1:2]
|
||||
|
||||
min_si = torch.ones(si.size()).cuda(self.device) * 0.1
|
||||
si = torch.max(min_si, si)
|
||||
norm = xx - mu
|
||||
term_a = (norm / si)**2 / 2
|
||||
term_b = torch.log(si * math.sqrt(2 * math.pi))
|
||||
|
||||
norm_si = (np.mean(np.abs(norm.cpu().detach().numpy())), np.mean(si.cpu().detach().numpy()))
|
||||
|
||||
if self.evaluate:
|
||||
return norm_si
|
||||
|
||||
return term_a + term_b
|
||||
|
||||
def forward(self, outputs, targets):
|
||||
|
||||
values = self.gaussian_1d(outputs, targets)
|
||||
|
||||
if not self.reduce or self.evaluate:
|
||||
return values
|
||||
if self.size_average:
|
||||
mean_values = torch.mean(values)
|
||||
return mean_values
|
||||
return torch.sum(values)
|
||||
@ -9,13 +9,15 @@ import math
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ..utils import get_iou_matches, reorder_matches, get_keypoints, pixel_to_camera, xyz_from_distance
|
||||
from ..utils import get_iou_matches, reorder_matches, get_keypoints, pixel_to_camera, xyz_from_distance, \
|
||||
mask_joint_disparity
|
||||
from .process import preprocess_monstereo, preprocess_monoloco, extract_outputs, extract_outputs_mono,\
|
||||
filter_outputs, cluster_outputs, unnormalize_bi
|
||||
filter_outputs, cluster_outputs, unnormalize_bi, laplace_sampling
|
||||
from ..activity import social_interactions
|
||||
from .architectures import MonolocoModel, MonStereoModel
|
||||
from .architectures import MonolocoModel, LocoModel
|
||||
|
||||
|
||||
class Loco:
|
||||
@ -69,7 +71,7 @@ class Loco:
|
||||
self.model = MonolocoModel(p_dropout=p_dropout, input_size=input_size, linear_size=linear_size,
|
||||
output_size=output_size)
|
||||
else:
|
||||
self.model = MonStereoModel(p_dropout=p_dropout, input_size=input_size, output_size=output_size,
|
||||
self.model = LocoModel(p_dropout=p_dropout, input_size=input_size, output_size=output_size,
|
||||
linear_size=linear_size, device=self.device)
|
||||
|
||||
self.model.load_state_dict(torch.load(model_path, map_location=lambda storage, loc: storage))
|
||||
@ -116,7 +118,7 @@ class Loco:
|
||||
outputs = self.model(inputs)
|
||||
|
||||
outputs = cluster_outputs(outputs, keypoints_r.shape[0])
|
||||
outputs_fin, mask = filter_outputs(outputs)
|
||||
outputs_fin, _ = filter_outputs(outputs)
|
||||
dic_out = extract_outputs(outputs_fin)
|
||||
|
||||
# For Median baseline
|
||||
@ -136,7 +138,6 @@ class Loco:
|
||||
Apply dropout at test time to obtain combined aleatoric + epistemic uncertainty
|
||||
"""
|
||||
assert self.net in ('monoloco', 'monoloco_p', 'monoloco_pp'), "Not supported for MonStereo"
|
||||
from .process import laplace_sampling
|
||||
|
||||
self.model.dropout.training = True # Manually reactivate dropout in eval
|
||||
total_outputs = torch.empty((0, inputs.size()[0])).to(self.device)
|
||||
@ -271,8 +272,6 @@ def median_disparity(dic_out, keypoints, keypoints_r, mask):
|
||||
Filters are applied to masks nan joints and remove outlier disparities with iqr
|
||||
The mask input is used to filter the all-vs-all approach
|
||||
"""
|
||||
import numpy as np
|
||||
from ..utils import mask_joint_disparity
|
||||
|
||||
keypoints = keypoints.cpu().numpy()
|
||||
keypoints_r = keypoints_r.cpu().numpy()
|
||||
|
||||
@ -7,7 +7,7 @@ import numpy as np
|
||||
import torch
|
||||
import torchvision
|
||||
|
||||
from ..utils import get_keypoints, pixel_to_camera, to_cartesian, back_correct_angles
|
||||
from ..utils import get_keypoints, pixel_to_camera, to_cartesian, back_correct_angles, open_annotations
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -30,7 +30,7 @@ def preprocess_monstereo(keypoints, keypoints_r, kk):
|
||||
inputs_r = preprocess_monoloco(keypoints_r, kk)
|
||||
|
||||
inputs = torch.empty((0, 68)).to(inputs_l.device)
|
||||
for idx, inp_l in enumerate(inputs_l.split(1)):
|
||||
for inp_l in inputs_l.split(1):
|
||||
clst = 0
|
||||
# inp_l = torch.cat((inp_l, cat[:, idx:idx+1]), dim=1)
|
||||
for idx_r, inp_r in enumerate(inputs_r.split(1)):
|
||||
@ -135,7 +135,6 @@ def preprocess_mask(dir_ann, basename, mode='left'):
|
||||
elif mode == 'right':
|
||||
path_ann = os.path.join(dir_ann + '_right', basename + '.json')
|
||||
|
||||
from ..utils import open_annotations
|
||||
dic = open_annotations(path_ann)
|
||||
if isinstance(dic, list):
|
||||
return [], []
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
# pylint: disable=too-many-statements, too-many-branches, undefined-loop-variable
|
||||
|
||||
"""
|
||||
Adapted from https://github.com/vita-epfl/openpifpaf/blob/master/openpifpaf/predict.py
|
||||
Adapted from https://github.com/openpifpaf/openpifpaf/blob/main/openpifpaf/predict.py,
|
||||
which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
and licensed under GNU AGPLv3
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import json
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
@ -17,7 +20,11 @@ import openpifpaf
|
||||
import openpifpaf.datasets as datasets
|
||||
from openpifpaf.predict import processor_factory, preprocess_factory
|
||||
from openpifpaf import decoder, network, visualizer, show, logger
|
||||
|
||||
try:
|
||||
import gdown
|
||||
DOWNLOAD = copy.copy(gdown.download)
|
||||
except ImportError:
|
||||
DOWNLOAD = None
|
||||
from .visuals.printer import Printer
|
||||
from .network import Loco
|
||||
from .network.process import factory_for_gt, preprocess_pifpaf
|
||||
@ -46,12 +53,15 @@ def get_torch_checkpoints_dir():
|
||||
|
||||
def download_checkpoints(args):
|
||||
torch_dir = get_torch_checkpoints_dir()
|
||||
pifpaf_model = os.path.join(torch_dir, 'shufflenetv2k30-201104-224654-cocokp-d75ed641.pkl')
|
||||
if args.checkpoint is None:
|
||||
pifpaf_model = os.path.join(torch_dir, 'shufflenetv2k30-201104-224654-cocokp-d75ed641.pkl')
|
||||
else:
|
||||
pifpaf_model = args.checkpoint
|
||||
dic_models = {'keypoints': pifpaf_model}
|
||||
if not os.path.exists(pifpaf_model):
|
||||
import gdown
|
||||
assert DOWNLOAD is not None, "pip install gdown to download pifpaf model, or pass it as --checkpoint"
|
||||
LOG.info('Downloading OpenPifPaf model in %s', torch_dir)
|
||||
gdown.download(OPENPIFPAF_MODEL, pifpaf_model, quiet=False)
|
||||
DOWNLOAD(OPENPIFPAF_MODEL, pifpaf_model, quiet=False)
|
||||
|
||||
if args.mode == 'keypoints':
|
||||
return dic_models
|
||||
@ -73,9 +83,9 @@ def download_checkpoints(args):
|
||||
model = os.path.join(torch_dir, name)
|
||||
dic_models[args.mode] = model
|
||||
if not os.path.exists(model):
|
||||
import gdown
|
||||
assert DOWNLOAD is not None, "pip install gdown to download monoloco model, or pass it as --model"
|
||||
LOG.info('Downloading model in %s', torch_dir)
|
||||
gdown.download(path, model, quiet=False)
|
||||
DOWNLOAD(path, model, quiet=False)
|
||||
return dic_models
|
||||
|
||||
|
||||
@ -209,7 +219,7 @@ def predict(args):
|
||||
|
||||
else:
|
||||
LOG.info("Prediction with MonStereo")
|
||||
boxes_r, keypoints_r = preprocess_pifpaf(pifpaf_outs['right'], im_size)
|
||||
_, keypoints_r = preprocess_pifpaf(pifpaf_outs['right'], im_size)
|
||||
dic_out = net.forward(keypoints, kk, keypoints_r=keypoints_r)
|
||||
dic_out = net.post_process(dic_out, boxes, keypoints, kk, dic_gt)
|
||||
|
||||
@ -229,15 +239,19 @@ def factory_outputs(args, pifpaf_outs, dic_out, output_path, kk=None):
|
||||
# Verify conflicting options
|
||||
if any((xx in args.output_types for xx in ['front', 'bird', 'multi'])):
|
||||
assert args.mode != 'keypoints', "for keypoints please use pifpaf original arguments"
|
||||
if args.social_distance:
|
||||
assert args.mode == 'mono', "Social distancing only works with monocular network"
|
||||
else:
|
||||
assert 'json' in args.output_types or args.mode == 'keypoints', \
|
||||
"No output saved, please select one among front, bird, multi, json, or pifpaf arguments"
|
||||
if args.social_distance:
|
||||
assert args.mode == 'mono', "Social distancing only works with monocular network"
|
||||
|
||||
if args.mode == 'keypoints':
|
||||
annotation_painter = openpifpaf.show.AnnotationPainter()
|
||||
with openpifpaf.show.image_canvas(pifpaf_outs['image'], output_path) as ax:
|
||||
annotation_painter.annotations(ax, pifpaf_outs['pred'])
|
||||
return
|
||||
|
||||
elif any((xx in args.output_types for xx in ['front', 'bird', 'multi'])):
|
||||
if any((xx in args.output_types for xx in ['front', 'bird', 'multi'])):
|
||||
LOG.info(output_path)
|
||||
if args.social_distance:
|
||||
show_social(args, pifpaf_outs['image'], output_path, pifpaf_outs['left'], dic_out)
|
||||
@ -246,9 +260,6 @@ def factory_outputs(args, pifpaf_outs, dic_out, output_path, kk=None):
|
||||
figures, axes = printer.factory_axes(dic_out)
|
||||
printer.draw(figures, axes, pifpaf_outs['image'])
|
||||
|
||||
elif 'json' in args.output_types:
|
||||
if 'json' in args.output_types:
|
||||
with open(os.path.join(output_path + '.monoloco.json'), 'w') as ff:
|
||||
json.dump(dic_out, ff)
|
||||
|
||||
else:
|
||||
LOG.info("No output saved, please select one among front, bird, multi, or pifpaf options")
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
|
||||
from .preprocess_kitti import parse_ground_truth, factory_file
|
||||
@ -1,350 +0,0 @@
|
||||
|
||||
# pylint: disable=too-many-statements, too-many-branches, too-many-nested-blocks
|
||||
|
||||
"""Preprocess annotations with KITTI ground-truth"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import datetime
|
||||
from PIL import Image
|
||||
|
||||
import torch
|
||||
import cv2
|
||||
|
||||
from ..utils import split_training, parse_ground_truth, get_iou_matches, append_cluster, factory_file, \
|
||||
extract_stereo_matches, get_category, normalize_hwl, make_new_directory
|
||||
from ..network.process import preprocess_pifpaf, preprocess_monoloco
|
||||
from .transforms import flip_inputs, flip_labels, height_augmentation
|
||||
|
||||
|
||||
class PreprocessKitti:
|
||||
"""Prepare arrays with same format as nuScenes preprocessing but using ground truth txt files"""
|
||||
|
||||
dir_gt = os.path.join('data', 'kitti', 'gt')
|
||||
dir_images = '/data/lorenzo-data/kitti/original_images/training/image_2'
|
||||
dir_byc_l = '/data/lorenzo-data/kitti/object_detection/left'
|
||||
|
||||
# SOCIAL DISTANCING PARAMETERS
|
||||
THRESHOLD_DIST = 2 # Threshold to check distance of people
|
||||
RADII = (0.3, 0.5, 1) # expected radii of the o-space
|
||||
SOCIAL_DISTANCE = True
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
dic_jo = {'train': dict(X=[], Y=[], names=[], kps=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list))),
|
||||
'val': dict(X=[], Y=[], names=[], kps=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list))),
|
||||
'test': dict(X=[], Y=[], names=[], kps=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list)))}
|
||||
dic_names = defaultdict(lambda: defaultdict(list))
|
||||
dic_std = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
def __init__(self, dir_ann, mode='mono', iou_min=0.3):
|
||||
|
||||
self.dir_ann = dir_ann
|
||||
self.iou_min = iou_min
|
||||
self.mode = mode
|
||||
assert self.mode in ('mono', 'stereo'), "modality not recognized"
|
||||
self.names_gt = tuple(os.listdir(self.dir_gt))
|
||||
self.dir_kk = os.path.join('data', 'kitti', 'calib')
|
||||
self.list_gt = glob.glob(self.dir_gt + '/*.txt')
|
||||
assert os.path.exists(self.dir_gt), "Ground truth dir does not exist"
|
||||
assert os.path.exists(self.dir_ann), "Annotation dir does not exist"
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now_time = now.strftime("%Y%m%d-%H%M")[2:]
|
||||
dir_out = os.path.join('data', 'arrays')
|
||||
self.path_joints = os.path.join(dir_out, 'joints-kitti-' + now_time + '.json')
|
||||
self.path_names = os.path.join(dir_out, 'names-kitti-' + now_time + '.json')
|
||||
path_train = os.path.join('splits', 'kitti_train.txt')
|
||||
path_val = os.path.join('splits', 'kitti_val.txt')
|
||||
self.set_train, self.set_val = split_training(self.names_gt, path_train, path_val)
|
||||
|
||||
def run(self):
|
||||
|
||||
cnt_match_l, cnt_match_r, cnt_pair, cnt_pair_tot, cnt_extra_pair, cnt_files, cnt_files_ped, cnt_fnf, \
|
||||
cnt_tot, cnt_ambiguous, cnt_cyclist = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
cnt_mono = {'train': 0, 'val': 0, 'test': 0}
|
||||
cnt_gt = cnt_mono.copy()
|
||||
cnt_stereo = cnt_mono.copy()
|
||||
correct_ped, correct_byc, wrong_ped, wrong_byc = 0, 0, 0, 0
|
||||
cnt_30, cnt_less_30 = 0, 0
|
||||
|
||||
# self.names_gt = ('002282.txt',)
|
||||
for name in self.names_gt:
|
||||
path_gt = os.path.join(self.dir_gt, name)
|
||||
basename, _ = os.path.splitext(name)
|
||||
path_im = os.path.join(self.dir_images, basename + '.png')
|
||||
phase, flag = self._factory_phase(name)
|
||||
if flag:
|
||||
cnt_fnf += 1
|
||||
continue
|
||||
|
||||
if phase == 'train':
|
||||
min_conf = 0
|
||||
category = 'all'
|
||||
else: # Remove for original results
|
||||
min_conf = 0.1
|
||||
category = 'pedestrian'
|
||||
|
||||
# Extract ground truth
|
||||
boxes_gt, ys, _, _ = parse_ground_truth(path_gt, # pylint: disable=unbalanced-tuple-unpacking
|
||||
category=category,
|
||||
spherical=True)
|
||||
cnt_gt[phase] += len(boxes_gt)
|
||||
cnt_files += 1
|
||||
cnt_files_ped += min(len(boxes_gt), 1) # if no boxes 0 else 1
|
||||
|
||||
# Extract keypoints
|
||||
path_calib = os.path.join(self.dir_kk, basename + '.txt')
|
||||
annotations, kk, tt = factory_file(path_calib, self.dir_ann, basename)
|
||||
|
||||
self.dic_names[basename + '.png']['boxes'] = copy.deepcopy(boxes_gt)
|
||||
self.dic_names[basename + '.png']['ys'] = copy.deepcopy(ys)
|
||||
self.dic_names[basename + '.png']['K'] = copy.deepcopy(kk)
|
||||
|
||||
# Check image size
|
||||
with Image.open(path_im) as im:
|
||||
width, height = im.size
|
||||
|
||||
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(width, height), min_conf=min_conf)
|
||||
|
||||
if keypoints:
|
||||
annotations_r, kk_r, tt_r = factory_file(path_calib, self.dir_ann, basename, mode='right')
|
||||
boxes_r, keypoints_r = preprocess_pifpaf(annotations_r, im_size=(width, height), min_conf=min_conf)
|
||||
cat = get_category(keypoints, os.path.join(self.dir_byc_l, basename + '.json'))
|
||||
|
||||
if not keypoints_r: # Case of no detection
|
||||
all_boxes_gt, all_ys = [boxes_gt], [ys]
|
||||
boxes_r, keypoints_r = boxes[0:1].copy(), keypoints[0:1].copy()
|
||||
all_boxes, all_keypoints = [boxes], [keypoints]
|
||||
all_keypoints_r = [keypoints_r]
|
||||
else:
|
||||
|
||||
# Horizontal Flipping for training
|
||||
if phase == 'train':
|
||||
# GT)
|
||||
boxes_gt_flip, ys_flip = flip_labels(boxes_gt, ys, im_w=width)
|
||||
# New left
|
||||
boxes_flip = flip_inputs(boxes_r, im_w=width, mode='box')
|
||||
keypoints_flip = flip_inputs(keypoints_r, im_w=width)
|
||||
|
||||
# New right
|
||||
keypoints_r_flip = flip_inputs(keypoints, im_w=width)
|
||||
|
||||
# combine the 2 modes
|
||||
all_boxes_gt = [boxes_gt, boxes_gt_flip]
|
||||
all_ys = [ys, ys_flip]
|
||||
all_boxes = [boxes, boxes_flip]
|
||||
all_keypoints = [keypoints, keypoints_flip]
|
||||
all_keypoints_r = [keypoints_r, keypoints_r_flip]
|
||||
|
||||
else:
|
||||
all_boxes_gt, all_ys = [boxes_gt], [ys]
|
||||
all_boxes, all_keypoints = [boxes], [keypoints]
|
||||
all_keypoints_r = [keypoints_r]
|
||||
|
||||
# Match each set of keypoint with a ground truth
|
||||
self.dic_jo[phase]['K'].append(kk)
|
||||
for ii, boxes_gt in enumerate(all_boxes_gt):
|
||||
keypoints, keypoints_r = torch.tensor(all_keypoints[ii]), torch.tensor(all_keypoints_r[ii])
|
||||
ys = all_ys[ii]
|
||||
matches = get_iou_matches(all_boxes[ii], boxes_gt, self.iou_min)
|
||||
for (idx, idx_gt) in matches:
|
||||
keypoint = keypoints[idx:idx + 1]
|
||||
lab = ys[idx_gt][:-1]
|
||||
|
||||
# Preprocess MonoLoco++
|
||||
if self.mode == 'mono':
|
||||
inp = preprocess_monoloco(keypoint, kk).view(-1).tolist()
|
||||
lab = normalize_hwl(lab)
|
||||
if ys[idx_gt][10] < 0.5:
|
||||
self.dic_jo[phase]['kps'].append(keypoint.tolist())
|
||||
self.dic_jo[phase]['X'].append(inp)
|
||||
self.dic_jo[phase]['Y'].append(lab)
|
||||
self.dic_jo[phase]['names'].append(name) # One image name for each annotation
|
||||
append_cluster(self.dic_jo, phase, inp, lab, keypoint.tolist())
|
||||
cnt_mono[phase] += 1
|
||||
cnt_tot += 1
|
||||
|
||||
# Preprocess MonStereo
|
||||
else:
|
||||
zz = ys[idx_gt][2]
|
||||
stereo_matches, cnt_amb = extract_stereo_matches(keypoint, keypoints_r, zz,
|
||||
phase=phase, seed=cnt_pair_tot)
|
||||
cnt_match_l += 1 if ii < 0.1 else 0 # matched instances
|
||||
cnt_match_r += 1 if ii > 0.9 else 0
|
||||
cnt_ambiguous += cnt_amb
|
||||
|
||||
# Monitor precision of classes
|
||||
if phase == 'val':
|
||||
if ys[idx_gt][10] == cat[idx] == 1:
|
||||
correct_byc += 1
|
||||
elif ys[idx_gt][10] == cat[idx] == 0:
|
||||
correct_ped += 1
|
||||
elif ys[idx_gt][10] != cat[idx] and ys[idx_gt][10] == 1:
|
||||
wrong_byc += 1
|
||||
elif ys[idx_gt][10] != cat[idx] and ys[idx_gt][10] == 0:
|
||||
wrong_ped += 1
|
||||
|
||||
cnt_cyclist += 1 if ys[idx_gt][10] == 1 else 0
|
||||
|
||||
for num, (idx_r, s_match) in enumerate(stereo_matches):
|
||||
label = ys[idx_gt][:-1] + [s_match]
|
||||
if s_match > 0.9:
|
||||
cnt_pair += 1
|
||||
|
||||
# Remove noise of very far instances for validation
|
||||
# if (phase == 'val') and (ys[idx_gt][3] >= 50):
|
||||
# continue
|
||||
|
||||
# ---> Save only positives unless there is no positive (keep positive flip and augm)
|
||||
# if num > 0 and s_match < 0.9:
|
||||
# continue
|
||||
|
||||
# Height augmentation
|
||||
cnt_pair_tot += 1
|
||||
cnt_extra_pair += 1 if ii == 1 else 0
|
||||
flag_aug = False
|
||||
if phase == 'train' and 3 < label[2] < 30 and s_match > 0.9:
|
||||
flag_aug = True
|
||||
elif phase == 'train' and 3 < label[2] < 30 and cnt_pair_tot % 2 == 0:
|
||||
flag_aug = True
|
||||
|
||||
# Remove height augmentation
|
||||
# flag_aug = False
|
||||
|
||||
if flag_aug:
|
||||
kps_aug, labels_aug = height_augmentation(
|
||||
keypoints[idx:idx+1], keypoints_r[idx_r:idx_r+1], label, s_match,
|
||||
seed=cnt_pair_tot)
|
||||
else:
|
||||
kps_aug = [(keypoints[idx:idx+1], keypoints_r[idx_r:idx_r+1])]
|
||||
labels_aug = [label]
|
||||
|
||||
for i, lab in enumerate(labels_aug):
|
||||
(kps, kps_r) = kps_aug[i]
|
||||
input_l = preprocess_monoloco(kps, kk).view(-1)
|
||||
input_r = preprocess_monoloco(kps_r, kk).view(-1)
|
||||
keypoint = torch.cat((kps, kps_r), dim=2).tolist()
|
||||
inp = torch.cat((input_l, input_l - input_r)).tolist()
|
||||
|
||||
# Only relative distances
|
||||
# inp_x = input[::2]
|
||||
# inp = torch.cat((inp_x, input - input_r)).tolist()
|
||||
|
||||
# lab = normalize_hwl(lab)
|
||||
if ys[idx_gt][10] < 0.5:
|
||||
self.dic_jo[phase]['kps'].append(keypoint)
|
||||
self.dic_jo[phase]['X'].append(inp)
|
||||
self.dic_jo[phase]['Y'].append(lab)
|
||||
self.dic_jo[phase]['names'].append(name) # One image name for each annotation
|
||||
append_cluster(self.dic_jo, phase, inp, lab, keypoint)
|
||||
cnt_tot += 1
|
||||
if s_match > 0.9:
|
||||
cnt_stereo[phase] += 1
|
||||
else:
|
||||
cnt_mono[phase] += 1
|
||||
|
||||
with open(self.path_joints, 'w') as file:
|
||||
json.dump(self.dic_jo, file)
|
||||
with open(os.path.join(self.path_names), 'w') as file:
|
||||
json.dump(self.dic_names, file)
|
||||
|
||||
# cout
|
||||
print(cnt_30)
|
||||
print(cnt_less_30)
|
||||
print('-' * 120)
|
||||
|
||||
print("Number of GT files: {}. Files with at least one pedestrian: {}. Files not found: {}"
|
||||
.format(cnt_files, cnt_files_ped, cnt_fnf))
|
||||
print("Ground truth matches : {:.1f} % for left images (train and val) and {:.1f} % for right images (train)"
|
||||
.format(100*cnt_match_l / (cnt_gt['train'] + cnt_gt['val']), 100*cnt_match_r / cnt_gt['train']))
|
||||
print("Total annotations: {}".format(cnt_tot))
|
||||
print("Total number of cyclists: {}\n".format(cnt_cyclist))
|
||||
print("Ambiguous instances removed: {}".format(cnt_ambiguous))
|
||||
print("Extra pairs created with horizontal flipping: {}\n".format(cnt_extra_pair))
|
||||
|
||||
if self.mode == 'stereo':
|
||||
print('Instances with stereo correspondence: {:.1f}% '.format(100 * cnt_pair / cnt_pair_tot))
|
||||
for phase in ['train', 'val']:
|
||||
cnt = cnt_mono[phase] + cnt_stereo[phase]
|
||||
print("{}: annotations: {}. Stereo pairs {:.1f}% "
|
||||
.format(phase.upper(), cnt, 100 * cnt_stereo[phase] / cnt))
|
||||
|
||||
print("\nOutput files:\n{}\n{}".format(self.path_names, self.path_joints))
|
||||
print('-' * 120)
|
||||
|
||||
def prep_activity(self):
|
||||
"""Augment ground-truth with flag activity"""
|
||||
|
||||
from monoloco.activity import social_interactions
|
||||
main_dir = os.path.join('data', 'kitti')
|
||||
dir_gt = os.path.join(main_dir, 'gt')
|
||||
dir_out = os.path.join(main_dir, 'gt_activity')
|
||||
make_new_directory(dir_out)
|
||||
cnt_tp, cnt_tn = 0, 0
|
||||
|
||||
# Extract validation images for evaluation
|
||||
category = 'pedestrian'
|
||||
|
||||
for name in self.set_val:
|
||||
# Read
|
||||
path_gt = os.path.join(dir_gt, name)
|
||||
boxes_gt, ys, truncs_gt, occs_gt, lines = parse_ground_truth(path_gt, category, spherical=False,
|
||||
verbose=True)
|
||||
angles = [y[10] for y in ys]
|
||||
dds = [y[4] for y in ys]
|
||||
xz_centers = [[y[0], y[2]] for y in ys]
|
||||
|
||||
# Write
|
||||
path_out = os.path.join(dir_out, name)
|
||||
with open(path_out, "w+") as ff:
|
||||
for idx, line in enumerate(lines):
|
||||
if social_interactions(idx, xz_centers, angles, dds,
|
||||
n_samples=1,
|
||||
threshold_dist=self.THRESHOLD_DIST,
|
||||
radii=self.RADII,
|
||||
social_distance=self.SOCIAL_DISTANCE):
|
||||
activity = '1'
|
||||
cnt_tp += 1
|
||||
else:
|
||||
activity = '0'
|
||||
cnt_tn += 1
|
||||
|
||||
line_new = line[:-1] + ' ' + activity + line[-1]
|
||||
ff.write(line_new)
|
||||
|
||||
print(f'Written {len(self.set_val)} new files in {dir_out}')
|
||||
print(f'Saved {cnt_tp} positive and {cnt_tn} negative annotations')
|
||||
|
||||
def _factory_phase(self, name):
|
||||
"""Choose the phase"""
|
||||
phase = None
|
||||
flag = False
|
||||
if name in self.set_train:
|
||||
phase = 'train'
|
||||
elif name in self.set_val:
|
||||
phase = 'val'
|
||||
else:
|
||||
flag = True
|
||||
return phase, flag
|
||||
|
||||
|
||||
def crop_and_draw(im, box, keypoint):
|
||||
|
||||
box = [round(el) for el in box[:-1]]
|
||||
center = (int((keypoint[0][0])), int((keypoint[1][0])))
|
||||
radius = round((box[3]-box[1]) / 20)
|
||||
im = cv2.circle(im, center, radius, color=(0, 255, 0), thickness=1)
|
||||
crop = im[box[1]:box[3], box[0]:box[2]]
|
||||
h_crop = crop.shape[0]
|
||||
w_crop = crop.shape[1]
|
||||
|
||||
return crop, h_crop, w_crop
|
||||
@ -1,132 +0,0 @@
|
||||
"""Preprocess annotations with KITTI ground-truth"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from .transforms import transform_keypoints
|
||||
from ..utils import get_calibration, split_training, parse_ground_truth, get_iou_matches, append_cluster
|
||||
from ..network.process import preprocess_pifpaf, preprocess_monoloco
|
||||
|
||||
|
||||
class PreprocessKitti:
|
||||
"""Prepare arrays with same format as nuScenes preprocessing but using ground truth txt files"""
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
dic_jo = {'train': dict(X=[], Y=[], names=[], kps=[], boxes_3d=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list))),
|
||||
'val': dict(X=[], Y=[], names=[], kps=[], boxes_3d=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list))),
|
||||
'test': dict(X=[], Y=[], names=[], kps=[], boxes_3d=[], K=[],
|
||||
clst=defaultdict(lambda: defaultdict(list)))}
|
||||
dic_names = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
def __init__(self, dir_ann, iou_min):
|
||||
|
||||
self.dir_ann = dir_ann
|
||||
self.iou_min = iou_min
|
||||
self.dir_gt = os.path.join('data', 'kitti', 'gt')
|
||||
self.names_gt = tuple(os.listdir(self.dir_gt))
|
||||
self.dir_kk = os.path.join('data', 'kitti', 'calib')
|
||||
self.list_gt = glob.glob(self.dir_gt + '/*.txt')
|
||||
assert os.path.exists(self.dir_gt), "Ground truth dir does not exist"
|
||||
assert os.path.exists(self.dir_ann), "Annotation dir does not exist"
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now_time = now.strftime("%Y%m%d-%H%M")[2:]
|
||||
dir_out = os.path.join('data', 'arrays')
|
||||
self.path_joints = os.path.join(dir_out, 'joints-kitti-' + now_time + '.json')
|
||||
self.path_names = os.path.join(dir_out, 'names-kitti-' + now_time + '.json')
|
||||
path_train = os.path.join('splits', 'kitti_train.txt')
|
||||
path_val = os.path.join('splits', 'kitti_val.txt')
|
||||
self.set_train, self.set_val = split_training(self.names_gt, path_train, path_val)
|
||||
|
||||
def run(self):
|
||||
"""Save json files"""
|
||||
|
||||
cnt_gt = cnt_files = cnt_files_ped = cnt_fnf = 0
|
||||
dic_cnt = {'train': 0, 'val': 0, 'test': 0}
|
||||
|
||||
for name in self.names_gt:
|
||||
path_gt = os.path.join(self.dir_gt, name)
|
||||
basename, _ = os.path.splitext(name)
|
||||
|
||||
phase, flag = self._factory_phase(name)
|
||||
if flag:
|
||||
cnt_fnf += 1
|
||||
continue
|
||||
|
||||
# Extract keypoints
|
||||
path_txt = os.path.join(self.dir_kk, basename + '.txt')
|
||||
p_left, _ = get_calibration(path_txt)
|
||||
kk = p_left[0]
|
||||
|
||||
# Iterate over each line of the gt file and save box location and distances
|
||||
boxes_gt, boxes_3d, dds_gt = parse_ground_truth(path_gt, category='all')[:3]
|
||||
|
||||
self.dic_names[basename + '.png']['boxes'] = copy.deepcopy(boxes_gt)
|
||||
self.dic_names[basename + '.png']['dds'] = copy.deepcopy(dds_gt)
|
||||
self.dic_names[basename + '.png']['K'] = copy.deepcopy(kk)
|
||||
cnt_gt += len(boxes_gt)
|
||||
cnt_files += 1
|
||||
cnt_files_ped += min(len(boxes_gt), 1) # if no boxes 0 else 1
|
||||
|
||||
# Find the annotations if exists
|
||||
try:
|
||||
with open(os.path.join(self.dir_ann, basename + '.png.pifpaf.json'), 'r') as f:
|
||||
annotations = json.load(f)
|
||||
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(1238, 374))
|
||||
keypoints_hflip = transform_keypoints(keypoints, mode='flip')
|
||||
inputs = preprocess_monoloco(keypoints, kk).tolist()
|
||||
inputs_hflip = preprocess_monoloco(keypoints_hflip, kk).tolist()
|
||||
all_keypoints = [keypoints, keypoints_hflip]
|
||||
all_inputs = [inputs, inputs_hflip]
|
||||
|
||||
except FileNotFoundError:
|
||||
boxes = []
|
||||
|
||||
# Match each set of keypoint with a ground truth
|
||||
matches = get_iou_matches(boxes, boxes_gt, self.iou_min)
|
||||
for (idx, idx_gt) in matches:
|
||||
for nn, keypoints in enumerate(all_keypoints):
|
||||
inputs = all_inputs[nn]
|
||||
self.dic_jo[phase]['kps'].append(keypoints[idx])
|
||||
self.dic_jo[phase]['X'].append(inputs[idx])
|
||||
self.dic_jo[phase]['Y'].append([dds_gt[idx_gt]]) # Trick to make it (nn,1)
|
||||
self.dic_jo[phase]['boxes_3d'].append(boxes_3d[idx_gt])
|
||||
self.dic_jo[phase]['K'].append(kk)
|
||||
self.dic_jo[phase]['names'].append(name) # One image name for each annotation
|
||||
append_cluster(self.dic_jo, phase, inputs[idx], dds_gt[idx_gt], keypoints[idx])
|
||||
dic_cnt[phase] += 1
|
||||
|
||||
with open(self.path_joints, 'w') as file:
|
||||
json.dump(self.dic_jo, file)
|
||||
with open(os.path.join(self.path_names), 'w') as file:
|
||||
json.dump(self.dic_names, file)
|
||||
for phase in ['train', 'val', 'test']:
|
||||
print("Saved {} annotations for phase {}"
|
||||
.format(dic_cnt[phase], phase))
|
||||
print("Number of GT files: {}. Files with at least one pedestrian: {}. Files not found: {}"
|
||||
.format(cnt_files, cnt_files_ped, cnt_fnf))
|
||||
print("Matched : {:.1f} % of the ground truth instances"
|
||||
.format(100 * (dic_cnt['train'] + dic_cnt['val']) / cnt_gt))
|
||||
print("\nOutput files:\n{}\n{}\n".format(self.path_names, self.path_joints))
|
||||
|
||||
def _factory_phase(self, name):
|
||||
"""Choose the phase"""
|
||||
|
||||
phase = None
|
||||
flag = False
|
||||
if name in self.set_train:
|
||||
phase = 'train'
|
||||
elif name in self.set_val:
|
||||
phase = 'val'
|
||||
else:
|
||||
flag = True
|
||||
return phase, flag
|
||||
392
monoloco/prep/preprocess_kitti.py
Normal file
392
monoloco/prep/preprocess_kitti.py
Normal file
@ -0,0 +1,392 @@
|
||||
# pylint: disable=too-many-statements, too-many-branches, too-many-nested-blocks
|
||||
|
||||
"""Preprocess annotations with KITTI ground-truth"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import copy
|
||||
import math
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import warnings
|
||||
import datetime
|
||||
from PIL import Image
|
||||
|
||||
import torch
|
||||
|
||||
from .. import __version__
|
||||
from ..utils import split_training, get_iou_matches, append_cluster, get_calibration, open_annotations, \
|
||||
extract_stereo_matches, make_new_directory, \
|
||||
check_conditions, to_spherical, correct_angle
|
||||
from ..network.process import preprocess_pifpaf, preprocess_monoloco
|
||||
from .transforms import flip_inputs, flip_labels, height_augmentation
|
||||
|
||||
|
||||
class PreprocessKitti:
|
||||
"""Prepare arrays with same format as nuScenes preprocessing but using ground truth txt files"""
|
||||
|
||||
# KITTI Dataset files
|
||||
dir_gt = os.path.join('data', 'kitti', 'gt')
|
||||
dir_images = os.path.join('data', 'kitti', 'images')
|
||||
dir_kk = os.path.join('data', 'kitti', 'calib')
|
||||
|
||||
# SOCIAL DISTANCING PARAMETERS
|
||||
THRESHOLD_DIST = 2 # Threshold to check distance of people
|
||||
RADII = (0.3, 0.5, 1) # expected radii of the o-space
|
||||
SOCIAL_DISTANCE = True
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
dic_jo = {
|
||||
'train': dict(X=[], Y=[], names=[], kps=[], K=[], clst=defaultdict(lambda: defaultdict(list))),
|
||||
'val': dict(X=[], Y=[], names=[], kps=[], K=[], clst=defaultdict(lambda: defaultdict(list))),
|
||||
'test': dict(X=[], Y=[], names=[], kps=[], K=[], clst=defaultdict(lambda: defaultdict(list))),
|
||||
'version': __version__,
|
||||
}
|
||||
dic_names = defaultdict(lambda: defaultdict(list))
|
||||
dic_std = defaultdict(lambda: defaultdict(list))
|
||||
categories_gt = dict(train=['Pedestrian', 'Person_sitting'], val=['Pedestrian'])
|
||||
|
||||
def __init__(self, dir_ann, mode='mono', iou_min=0.3, sample=False):
|
||||
|
||||
self.dir_ann = dir_ann
|
||||
self.mode = mode
|
||||
self.iou_min = iou_min
|
||||
self.sample = sample
|
||||
|
||||
assert os.path.isdir(self.dir_ann), "Annotation directory not found"
|
||||
assert any(os.scandir(self.dir_ann)), "Annotation directory empty"
|
||||
assert os.path.isdir(self.dir_gt), "Ground truth directory not found"
|
||||
assert any(os.scandir(self.dir_gt)), "Ground-truth directory empty"
|
||||
if self.mode == 'stereo':
|
||||
assert os.path.isdir(self.dir_ann + '_right'), "Annotation directory for right images not found"
|
||||
assert any(os.scandir(self.dir_ann + '_right')), "Annotation directory for right images empty"
|
||||
elif not os.path.isdir(self.dir_ann + '_right') or not any(os.scandir(self.dir_ann + '_right')):
|
||||
warnings.warn('Horizontal flipping not applied as annotation directory for right images not found/empty')
|
||||
assert self.mode in ('mono', 'stereo'), "modality not recognized"
|
||||
|
||||
self.names_gt = tuple(os.listdir(self.dir_gt))
|
||||
self.list_gt = glob.glob(self.dir_gt + '/*.txt')
|
||||
now = datetime.datetime.now()
|
||||
now_time = now.strftime("%Y%m%d-%H%M")[2:]
|
||||
dir_out = os.path.join('data', 'arrays')
|
||||
self.path_joints = os.path.join(dir_out, 'joints-kitti-' + self.mode + '-' + now_time + '.json')
|
||||
self.path_names = os.path.join(dir_out, 'names-kitti-' + self.mode + '-' + now_time + '.json')
|
||||
path_train = os.path.join('splits', 'kitti_train.txt')
|
||||
path_val = os.path.join('splits', 'kitti_val.txt')
|
||||
self.set_train, self.set_val = split_training(self.names_gt, path_train, path_val)
|
||||
self.phase, self.name = None, None
|
||||
self.stats = defaultdict(int)
|
||||
self.stats_stereo = defaultdict(int)
|
||||
|
||||
def run(self):
|
||||
# self.names_gt = ('002282.txt',)
|
||||
for self.name in self.names_gt:
|
||||
# Extract ground truth
|
||||
path_gt = os.path.join(self.dir_gt, self.name)
|
||||
basename, _ = os.path.splitext(self.name)
|
||||
self.phase, file_not_found = self._factory_phase(self.name)
|
||||
category = 'all' if self.phase == 'train' else 'pedestrian'
|
||||
if file_not_found:
|
||||
self.stats['fnf'] += 1
|
||||
continue
|
||||
|
||||
boxes_gt, labels, _, _, _ = parse_ground_truth(path_gt, category=category, spherical=True)
|
||||
self.stats['gt_' + self.phase] += len(boxes_gt)
|
||||
self.stats['gt_files'] += 1
|
||||
self.stats['gt_files_ped'] += min(len(boxes_gt), 1) # if no boxes 0 else 1
|
||||
self.dic_names[basename + '.png']['boxes'] = copy.deepcopy(boxes_gt)
|
||||
self.dic_names[basename + '.png']['ys'] = copy.deepcopy(labels)
|
||||
|
||||
# Extract annotations
|
||||
dic_boxes, dic_kps, dic_gt = self.parse_annotations(boxes_gt, labels, basename)
|
||||
if dic_boxes is None: # No annotations
|
||||
continue
|
||||
self.dic_names[basename + '.png']['K'] = copy.deepcopy(dic_gt['K'])
|
||||
self.dic_jo[self.phase]['K'].append(dic_gt['K'])
|
||||
|
||||
# Match each set of keypoint with a ground truth
|
||||
for ii, boxes_gt in enumerate(dic_boxes['gt']):
|
||||
kps, kps_r = torch.tensor(dic_kps['left'][ii]), torch.tensor(dic_kps['right'][ii])
|
||||
matches = get_iou_matches(dic_boxes['left'][ii], boxes_gt, self.iou_min)
|
||||
self.stats['flipping_match'] += len(matches) if ii == 1 else 0
|
||||
for (idx, idx_gt) in matches:
|
||||
cat_gt = dic_gt['labels'][ii][idx_gt][-1]
|
||||
if cat_gt not in self.categories_gt[self.phase]: # only for training as cyclists are also extracted
|
||||
continue
|
||||
kp = kps[idx:idx + 1]
|
||||
kk = dic_gt['K']
|
||||
label = dic_gt['labels'][ii][idx_gt][:-1]
|
||||
self.stats['match'] += 1
|
||||
assert len(label) == 10, 'dimensions of monocular label is wrong'
|
||||
|
||||
if self.mode == 'mono':
|
||||
self._process_annotation_mono(kp, kk, label)
|
||||
else:
|
||||
self._process_annotation_stereo(kp, kk, label, kps_r)
|
||||
|
||||
with open(self.path_joints, 'w') as file:
|
||||
json.dump(self.dic_jo, file)
|
||||
with open(os.path.join(self.path_names), 'w') as file:
|
||||
json.dump(self.dic_names, file)
|
||||
self._cout()
|
||||
|
||||
def parse_annotations(self, boxes_gt, labels, basename):
|
||||
|
||||
path_im = os.path.join(self.dir_images, basename + '.png')
|
||||
path_calib = os.path.join(self.dir_kk, basename + '.txt')
|
||||
min_conf = 0 if self.phase == 'train' else 0.1
|
||||
|
||||
# Check image size
|
||||
with Image.open(path_im) as im:
|
||||
width, height = im.size
|
||||
|
||||
# Extract left keypoints
|
||||
annotations, kk, _ = factory_file(path_calib, self.dir_ann, basename)
|
||||
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(width, height), min_conf=min_conf)
|
||||
if not keypoints:
|
||||
return None, None, None
|
||||
|
||||
# Stereo-based horizontal flipping for training (obtaining ground truth for right images)
|
||||
self.stats['instances'] += len(keypoints)
|
||||
annotations_r, _, _ = factory_file(path_calib, self.dir_ann, basename, ann_type='right')
|
||||
boxes_r, keypoints_r = preprocess_pifpaf(annotations_r, im_size=(width, height), min_conf=min_conf)
|
||||
|
||||
if not keypoints_r: # Duplicate the left one(s)
|
||||
all_boxes_gt, all_labels = [boxes_gt], [labels]
|
||||
boxes_r, keypoints_r = boxes[0:1].copy(), keypoints[0:1].copy()
|
||||
all_boxes, all_keypoints = [boxes], [keypoints]
|
||||
all_keypoints_r = [keypoints_r]
|
||||
|
||||
elif self.phase == 'train':
|
||||
# GT)
|
||||
boxes_gt_flip, ys_flip = flip_labels(boxes_gt, labels, im_w=width)
|
||||
# New left
|
||||
boxes_flip = flip_inputs(boxes_r, im_w=width, mode='box')
|
||||
keypoints_flip = flip_inputs(keypoints_r, im_w=width)
|
||||
|
||||
# New right
|
||||
keypoints_r_flip = flip_inputs(keypoints, im_w=width)
|
||||
|
||||
# combine the 2 modes
|
||||
all_boxes_gt = [boxes_gt, boxes_gt_flip]
|
||||
all_labels = [labels, ys_flip]
|
||||
all_boxes = [boxes, boxes_flip]
|
||||
all_keypoints = [keypoints, keypoints_flip]
|
||||
all_keypoints_r = [keypoints_r, keypoints_r_flip]
|
||||
|
||||
else:
|
||||
all_boxes_gt, all_labels = [boxes_gt], [labels]
|
||||
all_boxes, all_keypoints = [boxes], [keypoints]
|
||||
all_keypoints_r = [keypoints_r]
|
||||
|
||||
dic_boxes = dict(left=all_boxes, gt=all_boxes_gt)
|
||||
dic_kps = dict(left=all_keypoints, right=all_keypoints_r)
|
||||
dic_gt = dict(K=kk, labels=all_labels)
|
||||
return dic_boxes, dic_kps, dic_gt
|
||||
|
||||
def _process_annotation_mono(self, kp, kk, label):
|
||||
"""For a single annotation, process all the labels and save them"""
|
||||
kp = kp.tolist()
|
||||
inp = preprocess_monoloco(kp, kk).view(-1).tolist()
|
||||
|
||||
# Save
|
||||
self.dic_jo[self.phase]['kps'].append(kp)
|
||||
self.dic_jo[self.phase]['X'].append(inp)
|
||||
self.dic_jo[self.phase]['Y'].append(label)
|
||||
self.dic_jo[self.phase]['names'].append(self.name) # One image name for each annotation
|
||||
append_cluster(self.dic_jo, self.phase, inp, label, kp)
|
||||
self.stats['total_' + self.phase] += 1
|
||||
|
||||
def _process_annotation_stereo(self, kp, kk, label, kps_r):
|
||||
"""For a reference annotation, combine it with some (right) annotations and save it"""
|
||||
|
||||
zz = label[2]
|
||||
stereo_matches, cnt_amb = extract_stereo_matches(kp, kps_r, zz,
|
||||
phase=self.phase,
|
||||
seed=self.stats_stereo['pair'])
|
||||
self.stats_stereo['ambiguous'] += cnt_amb
|
||||
|
||||
for idx_r, s_match in stereo_matches:
|
||||
label_s = label + [s_match] # add flag to distinguish "true pairs and false pairs"
|
||||
self.stats_stereo['true_pair'] += 1 if s_match > 0.9 else 0
|
||||
self.stats_stereo['pair'] += 1 # before augmentation
|
||||
|
||||
# ---> Remove noise of very far instances for validation
|
||||
# if (self.phase == 'val') and (label[3] >= 50):
|
||||
# continue
|
||||
|
||||
# ---> Save only positives unless there is no positive (keep positive flip and augm)
|
||||
# if num > 0 and s_match < 0.9:
|
||||
# continue
|
||||
|
||||
# Height augmentation
|
||||
flag_aug = False
|
||||
if self.phase == 'train' and 3 < label[2] < 30 and (s_match > 0.9 or self.stats_stereo['pair'] % 2 == 0):
|
||||
flag_aug = True
|
||||
|
||||
# Remove height augmentation
|
||||
# flag_aug = False
|
||||
|
||||
if flag_aug:
|
||||
kps_aug, labels_aug = height_augmentation(kp, kps_r[idx_r:idx_r + 1], label_s,
|
||||
seed=self.stats_stereo['pair'])
|
||||
else:
|
||||
kps_aug = [(kp, kps_r[idx_r:idx_r + 1])]
|
||||
labels_aug = [label_s]
|
||||
|
||||
for i, lab in enumerate(labels_aug):
|
||||
assert len(lab) == 11, 'dimensions of stereo label is wrong'
|
||||
self.stats_stereo['pair_aug'] += 1
|
||||
(kp_aug, kp_aug_r) = kps_aug[i]
|
||||
input_l = preprocess_monoloco(kp_aug, kk).view(-1)
|
||||
input_r = preprocess_monoloco(kp_aug_r, kk).view(-1)
|
||||
keypoint = torch.cat((kp_aug, kp_aug_r), dim=2).tolist()
|
||||
inp = torch.cat((input_l, input_l - input_r)).tolist()
|
||||
self.dic_jo[self.phase]['kps'].append(keypoint)
|
||||
self.dic_jo[self.phase]['X'].append(inp)
|
||||
self.dic_jo[self.phase]['Y'].append(lab)
|
||||
self.dic_jo[self.phase]['names'].append(self.name) # One image name for each annotation
|
||||
append_cluster(self.dic_jo, self.phase, inp, lab, keypoint)
|
||||
self.stats_stereo['total_' + self.phase] += 1 # including height augmentation
|
||||
|
||||
def _cout(self):
|
||||
print('-' * 100)
|
||||
print(f"Number of GT files: {self.stats['gt_files']} ")
|
||||
print(f"Files with at least one pedestrian/cyclist: {self.stats['gt_files_ped']}")
|
||||
print(f"Files not found: {self.stats['fnf']}")
|
||||
print('-' * 100)
|
||||
our = self.stats['match'] - self.stats['flipping_match']
|
||||
gt = self.stats['gt_train'] + self.stats['gt_val']
|
||||
print(f"Ground truth matches: {100 * our / gt:.1f} for left images (train and val)")
|
||||
print(f"Parsed instances: {self.stats['instances']}")
|
||||
print(f"Ground truth instances: {gt}")
|
||||
print(f"Matched instances: {our}")
|
||||
print(f"Including horizontal flipping: {self.stats['match']}")
|
||||
|
||||
if self.mode == 'stereo':
|
||||
print('-' * 100)
|
||||
print(f"Ambiguous instances removed: {self.stats_stereo['ambiguous']}")
|
||||
print(f"True pairs ratio: {100 * self.stats_stereo['true_pair'] / self.stats_stereo['pair']:.1f}% ")
|
||||
print(f"Height augmentation pairs: {self.stats_stereo['pair_aug'] - self.stats_stereo['pair']} ")
|
||||
print('-' * 100)
|
||||
total_train = self.stats_stereo['total_train'] if self.mode == 'stereo' else self.stats['total_train']
|
||||
total_val = self.stats_stereo['total_val'] if self.mode == 'stereo' else self.stats['total_val']
|
||||
print(f"Total annotations for TRAINING: {total_train}")
|
||||
print(f"Total annotations for VALIDATION: {total_val}")
|
||||
print('-' * 100)
|
||||
print(f"\nOutput files:\n{self.path_names}\n{self.path_joints}")
|
||||
print('-' * 100)
|
||||
|
||||
def process_activity(self):
|
||||
"""Augment ground-truth with flag activity"""
|
||||
|
||||
from monoloco.activity import social_interactions # pylint: disable=import-outside-toplevel
|
||||
main_dir = os.path.join('data', 'kitti')
|
||||
dir_gt = os.path.join(main_dir, 'gt')
|
||||
dir_out = os.path.join(main_dir, 'gt_activity')
|
||||
make_new_directory(dir_out)
|
||||
cnt_tp, cnt_tn = 0, 0
|
||||
|
||||
# Extract validation images for evaluation
|
||||
category = 'pedestrian'
|
||||
|
||||
for name in self.set_val:
|
||||
# Read
|
||||
path_gt = os.path.join(dir_gt, name)
|
||||
_, ys, _, _, lines = parse_ground_truth(path_gt, category, spherical=False)
|
||||
angles = [y[10] for y in ys]
|
||||
dds = [y[4] for y in ys]
|
||||
xz_centers = [[y[0], y[2]] for y in ys]
|
||||
|
||||
# Write
|
||||
path_out = os.path.join(dir_out, name)
|
||||
with open(path_out, "w+") as ff:
|
||||
for idx, line in enumerate(lines):
|
||||
if social_interactions(idx, xz_centers, angles, dds,
|
||||
n_samples=1,
|
||||
threshold_dist=self.THRESHOLD_DIST,
|
||||
radii=self.RADII,
|
||||
social_distance=self.SOCIAL_DISTANCE):
|
||||
activity = '1'
|
||||
cnt_tp += 1
|
||||
else:
|
||||
activity = '0'
|
||||
cnt_tn += 1
|
||||
|
||||
line_new = line[:-1] + ' ' + activity + line[-1]
|
||||
ff.write(line_new)
|
||||
|
||||
print(f'Written {len(self.set_val)} new files in {dir_out}')
|
||||
print(f'Saved {cnt_tp} positive and {cnt_tn} negative annotations')
|
||||
|
||||
def _factory_phase(self, name):
|
||||
"""Choose the phase"""
|
||||
phase = None
|
||||
flag = False
|
||||
if name in self.set_train:
|
||||
phase = 'train'
|
||||
elif name in self.set_val:
|
||||
phase = 'val'
|
||||
else:
|
||||
flag = True
|
||||
return phase, flag
|
||||
|
||||
|
||||
def parse_ground_truth(path_gt, category, spherical=False):
|
||||
"""Parse KITTI ground truth files"""
|
||||
|
||||
boxes_gt = []
|
||||
labels = []
|
||||
truncs_gt = [] # Float from 0 to 1
|
||||
occs_gt = [] # Either 0,1,2,3 fully visible, partly occluded, largely occluded, unknown
|
||||
lines = []
|
||||
|
||||
with open(path_gt, "r") as f_gt:
|
||||
for line_gt in f_gt:
|
||||
line = line_gt.split()
|
||||
if not check_conditions(line_gt, category, method='gt'):
|
||||
continue
|
||||
truncs_gt.append(float(line[1]))
|
||||
occs_gt.append(int(line[2]))
|
||||
boxes_gt.append([float(x) for x in line[4:8]])
|
||||
xyz = [float(x) for x in line[11:14]]
|
||||
hwl = [float(x) for x in line[8:11]]
|
||||
dd = float(math.sqrt(xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))
|
||||
yaw = float(line[14])
|
||||
assert - math.pi <= yaw <= math.pi
|
||||
alpha = float(line[3])
|
||||
sin, cos, yaw_corr = correct_angle(yaw, xyz)
|
||||
assert min(abs(-yaw_corr - alpha), (abs(yaw_corr - alpha))) < 0.15, "more than 10 degrees of error"
|
||||
if spherical:
|
||||
rtp = to_spherical(xyz)
|
||||
loc = rtp[1:3] + xyz[2:3] + rtp[0:1] # [theta, psi, z, r]
|
||||
else:
|
||||
loc = xyz + [dd]
|
||||
cat = line[0] # 'Pedestrian', or 'Person_sitting' for people
|
||||
output = loc + hwl + [sin, cos, yaw, cat]
|
||||
labels.append(output)
|
||||
lines.append(line_gt)
|
||||
return boxes_gt, labels, truncs_gt, occs_gt, lines
|
||||
|
||||
|
||||
def factory_file(path_calib, dir_ann, basename, ann_type='left'):
|
||||
"""Choose the annotation and the calibration files"""
|
||||
|
||||
assert ann_type in ('left', 'right')
|
||||
p_left, p_right = get_calibration(path_calib)
|
||||
|
||||
if ann_type == 'left':
|
||||
kk, tt = p_left[:]
|
||||
path_ann = os.path.join(dir_ann, basename + '.png.predictions.json')
|
||||
|
||||
# The right folder is called <NameOfLeftFolder>_right
|
||||
else:
|
||||
kk, tt = p_right[:]
|
||||
path_ann = os.path.join(dir_ann + '_right', basename + '.png.predictions.json')
|
||||
|
||||
annotations = open_annotations(path_ann)
|
||||
|
||||
return annotations, kk, tt
|
||||
@ -150,7 +150,6 @@ def extract_ground_truth(boxes_obj, kk, spherical=True):
|
||||
ys = []
|
||||
|
||||
for box_obj in boxes_obj:
|
||||
|
||||
# Select category
|
||||
if box_obj.name[:6] != 'animal':
|
||||
general_name = box_obj.name.split('.')[0] + '.' + box_obj.name.split('.')[1]
|
||||
@ -248,8 +247,6 @@ def extract_social(inputs, ys, keypoints, idx, matches):
|
||||
all_inputs.extend(inputs[idx])
|
||||
|
||||
indices_idx = [idx for (idx, idx_gt) in matches]
|
||||
if len(sorted_indices) > 2:
|
||||
aa = 5
|
||||
for ii in range(1, 3):
|
||||
try:
|
||||
index = sorted_indices[ii]
|
||||
@ -266,17 +263,3 @@ def extract_social(inputs, ys, keypoints, idx, matches):
|
||||
all_inputs.extend([0.] * 2)
|
||||
assert len(all_inputs) == 34 + 2 * 2
|
||||
return all_inputs
|
||||
|
||||
|
||||
# def get_jean_yaw(box_obj):
|
||||
# b_corners = box_obj.bottom_corners()
|
||||
# center = box_obj.center
|
||||
# back_point = [(b_corners[0, 2] + b_corners[0, 3]) / 2, (b_corners[2, 2] + b_corners[2, 3]) / 2]
|
||||
#
|
||||
# x = b_corners[0, :] - back_point[0]
|
||||
# y = b_corners[2, :] - back_point[1]
|
||||
#
|
||||
# angle = math.atan2((x[0] + x[1]) / 2, (y[0] + y[1]) / 2) * 180 / 3.14
|
||||
# angle = (angle + 360) % 360
|
||||
# correction = math.atan2(center[0], center[2]) * 180 / 3.14
|
||||
# return angle, correction
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
|
||||
import math
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..utils import correct_angle, to_cartesian, to_spherical
|
||||
|
||||
BASELINE = 0.54
|
||||
BF = BASELINE * 721
|
||||
|
||||
@ -75,7 +78,6 @@ def flip_inputs(keypoints, im_w, mode=None):
|
||||
|
||||
def flip_labels(boxes_gt, labels, im_w):
|
||||
"""Correct x, d positions and angles after horizontal flipping"""
|
||||
from ..utils import correct_angle, to_cartesian, to_spherical
|
||||
boxes_flip = deepcopy(boxes_gt)
|
||||
labels_flip = deepcopy(labels)
|
||||
|
||||
@ -98,29 +100,28 @@ def flip_labels(boxes_gt, labels, im_w):
|
||||
yaw = label_flip[9]
|
||||
yaw_n = math.copysign(1, yaw) * (np.pi - abs(yaw)) # Horizontal flipping change of angle
|
||||
|
||||
sin, cos, yaw_corr = correct_angle(yaw_n, xyz)
|
||||
sin, cos, _ = correct_angle(yaw_n, xyz)
|
||||
label_flip[7], label_flip[8], label_flip[9] = sin, cos, yaw_n
|
||||
|
||||
return boxes_flip, labels_flip
|
||||
|
||||
|
||||
def height_augmentation(kps, kps_r, label, s_match, seed=0):
|
||||
def height_augmentation(kps, kps_r, label_s, seed=0):
|
||||
"""
|
||||
label: theta, psi, z, rho, wlh, sin, cos, yaw, cat
|
||||
label_s: theta, psi, z, rho, wlh, sin, cos, s_match
|
||||
"""
|
||||
from ..utils import to_cartesian
|
||||
n_labels = 3 if s_match > 0.9 else 1
|
||||
n_labels = 3 if label_s[-1] > 0.9 else 1
|
||||
height_min = 1.2
|
||||
height_max = 2
|
||||
av_height = 1.71
|
||||
kps_aug = [[kps.clone(), kps_r.clone()] for _ in range(n_labels+1)]
|
||||
labels_aug = [label.copy() for _ in range(n_labels+1)] # Maintain the original
|
||||
labels_aug = [label_s.copy() for _ in range(n_labels+1)] # Maintain the original
|
||||
np.random.seed(seed)
|
||||
heights = np.random.uniform(height_min, height_max, n_labels) # 3 samples
|
||||
zzs = heights * label[2] / av_height
|
||||
disp = BF / label[2]
|
||||
zzs = heights * label_s[2] / av_height
|
||||
disp = BF / label_s[2]
|
||||
|
||||
rtp = label[3:4] + label[0:2] # Originally t,p,z,r
|
||||
rtp = label_s[3:4] + label_s[0:2] # Originally t,p,z,r
|
||||
xyz = to_cartesian(rtp)
|
||||
|
||||
for i in range(n_labels):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
# pylint: disable=too-many-branches, too-many-statements, import-outside-toplevel
|
||||
|
||||
import argparse
|
||||
|
||||
@ -18,6 +18,7 @@ def cli():
|
||||
# Predict (2D pose and/or 3D location from images)
|
||||
predict_parser.add_argument('images', nargs='*', help='input images')
|
||||
predict_parser.add_argument('--glob', help='glob expression for input images (for many images)')
|
||||
predict_parser.add_argument('--checkpoint', help='pifpaf model')
|
||||
predict_parser.add_argument('-o', '--output-directory', help='Output directory')
|
||||
predict_parser.add_argument('--output_types', nargs='+', default=['json'],
|
||||
help='what to output: json keypoints skeleton for Pifpaf'
|
||||
@ -35,9 +36,10 @@ def cli():
|
||||
predict_parser.add_argument('--instance-threshold', type=float, default=None, help='threshold for entire instance')
|
||||
predict_parser.add_argument('--seed-threshold', type=float, default=0.5, help='threshold for single seed')
|
||||
predict_parser.add_argument('--disable-cuda', action='store_true', help='disable CUDA')
|
||||
predict_parser.add_argument('--focal',
|
||||
help='focal length in mm for a sensor size of 7.2x5.4 mm. Default nuScenes sensor',
|
||||
type=float, default=5.7)
|
||||
predict_parser.add_argument('--precise-rescaling', dest='fast_rescaling', default=True, action='store_false',
|
||||
help='use more exact image rescaling (requires scipy)')
|
||||
predict_parser.add_argument('--decoder-workers', default=None, type=int,
|
||||
help='number of workers for pose decoding, 0 for windows')
|
||||
|
||||
decoder.cli(parser)
|
||||
logger.cli(parser)
|
||||
@ -53,6 +55,8 @@ def cli():
|
||||
predict_parser.add_argument('--n_dropout', type=int, help='Epistemic uncertainty evaluation', default=0)
|
||||
predict_parser.add_argument('--dropout', type=float, help='dropout parameter', default=0.2)
|
||||
predict_parser.add_argument('--show_all', help='only predict ground-truth matches or all', action='store_true')
|
||||
predict_parser.add_argument('--focal', help='focal length in mm for a sensor size of 7.2x5.4 mm. (nuScenes)',
|
||||
type=float, default=5.7)
|
||||
|
||||
# Social distancing and social interactions
|
||||
predict_parser.add_argument('--social_distance', help='social', action='store_true')
|
||||
@ -74,11 +78,12 @@ def cli():
|
||||
# Training
|
||||
training_parser.add_argument('--joints', help='Json file with input joints', required=True)
|
||||
training_parser.add_argument('--mode', help='mono, stereo', default='mono')
|
||||
training_parser.add_argument('--out', help='output_path, e.g., data/outputs/test.pkl')
|
||||
training_parser.add_argument('-e', '--epochs', type=int, help='number of epochs to train for', default=500)
|
||||
training_parser.add_argument('--bs', type=int, default=512, help='input batch size')
|
||||
training_parser.add_argument('--monocular', help='whether to train monoloco', action='store_true')
|
||||
training_parser.add_argument('--dropout', type=float, help='dropout. Default no dropout', default=0.2)
|
||||
training_parser.add_argument('--lr', type=float, help='learning rate', default=0.001)
|
||||
training_parser.add_argument('--lr', type=float, help='learning rate', default=0.002)
|
||||
training_parser.add_argument('--sched_step', type=float, help='scheduler step time (epochs)', default=30)
|
||||
training_parser.add_argument('--sched_gamma', type=float, help='Scheduler multiplication every step', default=0.98)
|
||||
training_parser.add_argument('--hidden_size', type=int, help='Number of hidden units in the model', default=1024)
|
||||
@ -131,10 +136,10 @@ def main():
|
||||
prep = PreprocessNuscenes(args.dir_ann, args.dir_nuscenes, args.dataset, args.iou_min)
|
||||
prep.run()
|
||||
else:
|
||||
from .prep.prep_kitti import PreprocessKitti
|
||||
from .prep.preprocess_kitti import PreprocessKitti
|
||||
prep = PreprocessKitti(args.dir_ann, mode=args.mode, iou_min=args.iou_min)
|
||||
if args.activity:
|
||||
prep.prep_activity()
|
||||
prep.process_activity()
|
||||
else:
|
||||
prep.run()
|
||||
|
||||
@ -162,7 +167,7 @@ def main():
|
||||
|
||||
elif args.geometric:
|
||||
assert args.joints, "joints argument not provided"
|
||||
from .network.geom_baseline import geometric_baseline
|
||||
from .eval.geom_baseline import geometric_baseline
|
||||
geometric_baseline(args.joints)
|
||||
|
||||
elif args.variance:
|
||||
|
||||
@ -60,6 +60,7 @@ class KeypointsDataset(Dataset):
|
||||
self.outputs_all = torch.tensor(dic_jo[phase]['Y'])
|
||||
self.names_all = dic_jo[phase]['names']
|
||||
self.kps_all = torch.tensor(dic_jo[phase]['kps'])
|
||||
self.version = dic_jo['version']
|
||||
|
||||
# Extract annotations divided in clusters
|
||||
self.dic_clst = dic_jo[phase]['clst']
|
||||
@ -90,3 +91,6 @@ class KeypointsDataset(Dataset):
|
||||
count = len(self.dic_clst[clst]['Y'])
|
||||
|
||||
return inputs, outputs, count
|
||||
|
||||
def get_version(self):
|
||||
return self.version
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
"""Inspired by Openpifpaf"""
|
||||
"""
|
||||
Adapted from https://github.com/openpifpaf,
|
||||
which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
and licensed under GNU AGPLv3
|
||||
"""
|
||||
|
||||
|
||||
import math
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from ..network import extract_labels, extract_labels_aux, extract_outputs
|
||||
|
||||
@ -180,6 +186,58 @@ class GaussianLoss(torch.nn.Module):
|
||||
return torch.sum(values)
|
||||
|
||||
|
||||
class CustomL1Loss(torch.nn.Module):
|
||||
"""
|
||||
Experimental, not used.
|
||||
L1 loss with more weight to errors at a shorter distance
|
||||
It inherits from nn.module so it supports backward
|
||||
"""
|
||||
|
||||
def __init__(self, dic_norm, device, beta=1):
|
||||
super().__init__()
|
||||
|
||||
self.dic_norm = dic_norm
|
||||
self.device = device
|
||||
self.beta = beta
|
||||
|
||||
@staticmethod
|
||||
def compute_weights(xx, beta=1):
|
||||
"""
|
||||
Return the appropriate weight depending on the distance and the hyperparameter chosen
|
||||
alpha = 1 refers to the curve of A Photogrammetric Approach for Real-time...
|
||||
It is made for unnormalized outputs (to be more understandable)
|
||||
From 70 meters on every value is weighted the same (0.1**beta)
|
||||
Alpha is optional value from Focal loss. Yet to be analyzed
|
||||
"""
|
||||
# alpha = np.maximum(1, 10 ** (beta - 1))
|
||||
alpha = 1
|
||||
ww = np.maximum(0.1, 1 - xx / 78)**beta
|
||||
|
||||
return alpha * ww
|
||||
|
||||
def print_loss(self):
|
||||
xx = np.linspace(0, 80, 100)
|
||||
y1 = self.compute_weights(xx, beta=1)
|
||||
y2 = self.compute_weights(xx, beta=2)
|
||||
y3 = self.compute_weights(xx, beta=3)
|
||||
plt.plot(xx, y1)
|
||||
plt.plot(xx, y2)
|
||||
plt.plot(xx, y3)
|
||||
plt.xlabel("Distance [m]")
|
||||
plt.ylabel("Loss function Weight")
|
||||
plt.legend(("Beta = 1", "Beta = 2", "Beta = 3"))
|
||||
plt.show()
|
||||
|
||||
def forward(self, output, target):
|
||||
|
||||
unnormalized_output = output.cpu().detach().numpy() * self.dic_norm['std']['Y'] + self.dic_norm['mean']['Y']
|
||||
weights_np = self.compute_weights(unnormalized_output, self.beta)
|
||||
weights = torch.from_numpy(weights_np).float().to(self.device) # To make weights in the same cuda device
|
||||
losses = torch.abs(output - target) * weights
|
||||
loss = losses.mean() # Mean over the batch
|
||||
return loss
|
||||
|
||||
|
||||
def angle_loss(orient, gt_orient):
|
||||
"""Only for evaluation"""
|
||||
angles = torch.atan2(orient[:, 0], orient[:, 1])
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
"""
|
||||
Training and evaluation of a neural network which predicts 3D localization and confidence intervals
|
||||
given 2d joints
|
||||
Training and evaluation of a neural network that, given 2D joints, estimates:
|
||||
- 3D localization and confidence intervals
|
||||
- Orientation
|
||||
- Bounding box dimensions
|
||||
"""
|
||||
|
||||
import copy
|
||||
@ -12,7 +14,6 @@ import logging
|
||||
from collections import defaultdict
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from itertools import chain
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
@ -20,10 +21,11 @@ import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.optim import lr_scheduler
|
||||
|
||||
from .. import __version__
|
||||
from .datasets import KeypointsDataset
|
||||
from .losses import CompositeLoss, MultiTaskLoss, AutoTuneMultiTaskLoss
|
||||
from ..network import extract_outputs, extract_labels
|
||||
from ..network.architectures import MonStereoModel
|
||||
from ..network.architectures import LocoModel
|
||||
from ..utils import set_logger
|
||||
|
||||
|
||||
@ -35,21 +37,16 @@ class Trainer:
|
||||
val_task = 'd'
|
||||
lambdas = (1, 1, 1, 1, 1, 1, 1, 1)
|
||||
clusters = ['10', '20', '30', '40']
|
||||
input_size = dict(mono=34, stereo=68)
|
||||
output_size = dict(mono=9, stereo=10)
|
||||
dir_figures = os.path.join('figures', 'losses')
|
||||
|
||||
def __init__(self, args):
|
||||
"""
|
||||
Initialize directories, load the data and parameters for the training
|
||||
"""
|
||||
|
||||
# Initialize directories and parameters
|
||||
dir_out = os.path.join('data', 'models')
|
||||
if not os.path.exists(dir_out):
|
||||
warnings.warn("Warning: output directory not found, the model will not be saved")
|
||||
dir_logs = os.path.join('data', 'logs')
|
||||
if not os.path.exists(dir_logs):
|
||||
warnings.warn("Warning: default logs directory not found")
|
||||
assert os.path.exists(args.joints), "Input file not found"
|
||||
|
||||
self.mode = args.mode
|
||||
self.joints = args.joints
|
||||
self.num_epochs = args.epochs
|
||||
@ -60,10 +57,22 @@ class Trainer:
|
||||
self.sched_gamma = args.sched_gamma
|
||||
self.hidden_size = args.hidden_size
|
||||
self.n_stage = args.n_stage
|
||||
self.dir_out = dir_out
|
||||
self.r_seed = args.r_seed
|
||||
self.auto_tune_mtl = args.auto_tune_mtl
|
||||
|
||||
# Select path out
|
||||
if args.out:
|
||||
self.path_out = args.out # full path without extension
|
||||
dir_out, _ = os.path.split(self.path_out)
|
||||
else:
|
||||
dir_out = os.path.join('data', 'outputs')
|
||||
name = 'monoloco_pp' if self.mode == 'mono' else 'monstereo'
|
||||
now = datetime.datetime.now()
|
||||
now_time = now.strftime("%Y%m%d-%H%M")[2:]
|
||||
name_out = name + '-' + now_time + '.pkl'
|
||||
self.path_out = os.path.join(dir_out, name_out)
|
||||
assert dir_out, "Directory to save the model not found"
|
||||
print(self.path_out)
|
||||
# Select the device
|
||||
use_cuda = torch.cuda.is_available()
|
||||
self.device = torch.device("cuda" if use_cuda else "cpu")
|
||||
@ -85,45 +94,28 @@ class Trainer:
|
||||
self.mt_loss = MultiTaskLoss(losses_tr, losses_val, self.lambdas, self.tasks)
|
||||
self.mt_loss.to(self.device)
|
||||
|
||||
if self.mode == 'stereo':
|
||||
input_size = 68
|
||||
output_size = 10
|
||||
else:
|
||||
input_size = 34
|
||||
output_size = 9
|
||||
|
||||
name = 'monoloco_pp' if self.mode == 'mono' else 'monstereo'
|
||||
now = datetime.datetime.now()
|
||||
now_time = now.strftime("%Y%m%d-%H%M")[2:]
|
||||
name_out = name + '-' + now_time
|
||||
if not self.no_save:
|
||||
self.path_model = os.path.join(dir_out, name_out + '.pkl')
|
||||
self.logger = set_logger(os.path.join(dir_logs, name_out))
|
||||
self.logger.info( # pylint: disable=logging-fstring-interpolation
|
||||
f'Training arguments: \ninput_file: {self.joints} \nmode: {self.mode} '
|
||||
f'\nlearning rate: {args.lr} \nbatch_size: {args.bs}'
|
||||
f'\nepochs: {args.epochs} \ndropout: {args.dropout} '
|
||||
f'\nscheduler step: {args.sched_step} \nscheduler gamma: {args.sched_gamma} '
|
||||
f'\ninput_size: {input_size} \noutput_size: {output_size} \nhidden_size: {args.hidden_size}'
|
||||
f' \nn_stages: {args.n_stage} \n r_seed: {args.r_seed} \nlambdas: {self.lambdas}'
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Dataloader
|
||||
self.dataloaders = {phase: DataLoader(KeypointsDataset(self.joints, phase=phase),
|
||||
batch_size=args.bs, shuffle=True) for phase in ['train', 'val']}
|
||||
|
||||
self.dataset_sizes = {phase: len(KeypointsDataset(self.joints, phase=phase))
|
||||
for phase in ['train', 'val']}
|
||||
self.dataset_version = KeypointsDataset(self.joints, phase='train').get_version()
|
||||
|
||||
self._set_logger(args)
|
||||
|
||||
# Define the model
|
||||
self.logger.info('Sizes of the dataset: {}'.format(self.dataset_sizes))
|
||||
print(">>> creating model")
|
||||
|
||||
self.model = MonStereoModel(input_size=input_size, output_size=output_size, linear_size=args.hidden_size,
|
||||
p_dropout=args.dropout, num_stage=self.n_stage, device=self.device)
|
||||
self.model = LocoModel(
|
||||
input_size=self.input_size[self.mode],
|
||||
output_size=self.output_size[self.mode],
|
||||
linear_size=args.hidden_size,
|
||||
p_dropout=args.dropout,
|
||||
num_stage=self.n_stage,
|
||||
device=self.device,
|
||||
)
|
||||
self.model.to(self.device)
|
||||
print(">>> model params: {:.3f}M".format(sum(p.numel() for p in self.model.parameters()) / 1000000.0))
|
||||
print(">>> loss params: {}".format(sum(p.numel() for p in self.mt_loss.parameters())))
|
||||
@ -158,7 +150,7 @@ class Trainer:
|
||||
if phase == 'train':
|
||||
self.optimizer.zero_grad()
|
||||
outputs = self.model(inputs)
|
||||
loss, loss_values = self.mt_loss(outputs, labels, phase=phase)
|
||||
loss, _ = self.mt_loss(outputs, labels, phase=phase)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 3)
|
||||
self.optimizer.step()
|
||||
@ -188,8 +180,7 @@ class Trainer:
|
||||
self.logger.info('Best validation Accuracy for {}: {:.3f}'.format(self.val_task, best_acc))
|
||||
self.logger.info('Saved weights of the model at epoch: {}'.format(best_epoch))
|
||||
|
||||
if self.print_loss:
|
||||
print_losses(epoch_losses)
|
||||
self._print_losses(epoch_losses)
|
||||
|
||||
# load best model weights
|
||||
self.model.load_state_dict(best_model_wts)
|
||||
@ -255,7 +246,7 @@ class Trainer:
|
||||
def compute_stats(self, outputs, labels, dic_err, size_eval, clst):
|
||||
"""Compute mean, bi and max of torch tensors"""
|
||||
|
||||
loss, loss_values = self.mt_loss(outputs, labels, phase='val')
|
||||
_, loss_values = self.mt_loss(outputs, labels, phase='val')
|
||||
rel_frac = outputs.size(0) / size_eval
|
||||
|
||||
tasks = self.tasks[:-1] if self.tasks[-1] == 'aux' else self.tasks # Exclude auxiliary
|
||||
@ -333,6 +324,41 @@ class Trainer:
|
||||
if epoch % 10 == 0:
|
||||
print(string.format(*format_list))
|
||||
|
||||
def _print_losses(self, epoch_losses):
|
||||
if not self.print_loss:
|
||||
return
|
||||
os.makedirs(self.dir_figures, exist_ok=True)
|
||||
for idx, phase in enumerate(epoch_losses):
|
||||
for idx_2, el in enumerate(epoch_losses['train']):
|
||||
plt.figure(idx + idx_2)
|
||||
plt.title(phase + '_' + el)
|
||||
plt.xlabel('epochs')
|
||||
plt.plot(epoch_losses[phase][el][10:], label='{} Loss: {}'.format(phase, el))
|
||||
plt.savefig(os.path.join(self.dir_figures, '{}_loss_{}.png'.format(phase, el)))
|
||||
plt.close()
|
||||
|
||||
def _set_logger(self, args):
|
||||
if self.no_save:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
else:
|
||||
self.path_model = self.path_out
|
||||
print(self.path_model)
|
||||
self.logger = set_logger(os.path.splitext(self.path_out)[0]) # remove .pkl
|
||||
self.logger.info( # pylint: disable=logging-fstring-interpolation
|
||||
f'\nVERSION: {__version__}\n'
|
||||
f'\nINPUT_FILE: {args.joints}'
|
||||
f'\nInput file version: {self.dataset_version}'
|
||||
f'\nTorch version: {torch.__version__}\n'
|
||||
f'\nTraining arguments:'
|
||||
f'\nmode: {self.mode} \nlearning rate: {args.lr} \nbatch_size: {args.bs}'
|
||||
f'\nepochs: {args.epochs} \ndropout: {args.dropout} '
|
||||
f'\nscheduler step: {args.sched_step} \nscheduler gamma: {args.sched_gamma} '
|
||||
f'\ninput_size: {self.input_size[self.mode]} \noutput_size: {self.output_size[self.mode]} '
|
||||
f'\nhidden_size: {args.hidden_size}'
|
||||
f' \nn_stages: {args.n_stage} \n r_seed: {args.r_seed} \nlambdas: {self.lambdas}'
|
||||
)
|
||||
|
||||
|
||||
def debug_plots(inputs, labels):
|
||||
inputs_shoulder = inputs.cpu().numpy()[:, 5]
|
||||
@ -347,15 +373,6 @@ def debug_plots(inputs, labels):
|
||||
plt.show()
|
||||
|
||||
|
||||
def print_losses(epoch_losses):
|
||||
for idx, phase in enumerate(epoch_losses):
|
||||
for idx_2, el in enumerate(epoch_losses['train']):
|
||||
plt.figure(idx + idx_2)
|
||||
plt.plot(epoch_losses[phase][el][10:], label='{} Loss: {}'.format(phase, el))
|
||||
plt.savefig('figures/{}_loss_{}.png'.format(phase, el))
|
||||
plt.close()
|
||||
|
||||
|
||||
def get_accuracy(outputs, labels):
|
||||
"""From Binary cross entropy outputs to accuracy"""
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
|
||||
from .iou import get_iou_matches, reorder_matches, get_iou_matrix, get_iou_matches_matrix
|
||||
from .misc import get_task_error, get_pixel_error, append_cluster, open_annotations, make_new_directory,\
|
||||
from .iou import get_iou_matches, reorder_matches, get_iou_matrix, get_iou_matches_matrix, get_category, \
|
||||
open_annotations
|
||||
from .misc import get_task_error, get_pixel_error, append_cluster, make_new_directory,\
|
||||
normalize_hwl, average
|
||||
from .kitti import check_conditions, get_difficulty, split_training, parse_ground_truth, get_calibration, \
|
||||
factory_basename, factory_file, get_category, read_and_rewrite, find_cluster
|
||||
from .kitti import check_conditions, get_difficulty, split_training, get_calibration, \
|
||||
factory_basename, read_and_rewrite, find_cluster
|
||||
from .camera import xyz_from_distance, get_keypoints, pixel_to_camera, project_3d, open_image, correct_angle,\
|
||||
to_spherical, to_cartesian, back_correct_angles, project_to_pixels
|
||||
from .logs import set_logger
|
||||
from ..utils.nuscenes import select_categories
|
||||
from ..utils.stereo import mask_joint_disparity, average_locations, extract_stereo_matches, \
|
||||
from .nuscenes import select_categories
|
||||
from .stereo import mask_joint_disparity, average_locations, extract_stereo_matches, \
|
||||
verify_stereo, disparity_to_depth
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@ -52,7 +54,7 @@ def get_iou_matches(boxes, boxes_gt, iou_min=0.3):
|
||||
for idx in indices[::-1]:
|
||||
box = boxes[idx]
|
||||
ious = []
|
||||
for idx_gt, box_gt in enumerate(boxes_gt):
|
||||
for box_gt in boxes_gt:
|
||||
iou = calculate_iou(box, box_gt)
|
||||
ious.append(iou)
|
||||
idx_gt_max = int(np.argmax(ious))
|
||||
@ -96,3 +98,48 @@ def reorder_matches(matches, boxes, mode='left_rigth'):
|
||||
matches_left = [int(idx) for (idx, _) in matches]
|
||||
|
||||
return [matches[matches_left.index(idx_boxes)] for idx_boxes in ordered_boxes if idx_boxes in matches_left]
|
||||
|
||||
|
||||
def get_category(keypoints, path_byc):
|
||||
"""Find the category for each of the keypoints"""
|
||||
|
||||
dic_byc = open_annotations(path_byc)
|
||||
boxes_byc = dic_byc['boxes'] if dic_byc else []
|
||||
boxes_ped = make_lower_boxes(keypoints)
|
||||
|
||||
matches = get_matches_bikes(boxes_ped, boxes_byc)
|
||||
list_byc = [match[0] for match in matches]
|
||||
categories = [1.0 if idx in list_byc else 0.0 for idx, _ in enumerate(boxes_ped)]
|
||||
return categories
|
||||
|
||||
|
||||
def get_matches_bikes(boxes_ped, boxes_byc):
|
||||
matches = get_iou_matches_matrix(boxes_ped, boxes_byc, thresh=0.15)
|
||||
matches_b = []
|
||||
for idx, idx_byc in matches:
|
||||
box_ped = boxes_ped[idx]
|
||||
box_byc = boxes_byc[idx_byc]
|
||||
width_ped = box_ped[2] - box_ped[0]
|
||||
width_byc = box_byc[2] - box_byc[0]
|
||||
center_ped = (box_ped[2] + box_ped[0]) / 2
|
||||
center_byc = (box_byc[2] + box_byc[0]) / 2
|
||||
if abs(center_ped - center_byc) < min(width_ped, width_byc) / 4:
|
||||
matches_b.append((idx, idx_byc))
|
||||
return matches_b
|
||||
|
||||
|
||||
def make_lower_boxes(keypoints):
|
||||
lower_boxes = []
|
||||
keypoints = np.array(keypoints)
|
||||
for kps in keypoints:
|
||||
lower_boxes.append([min(kps[0, 9:]), min(kps[1, 9:]), max(kps[0, 9:]), max(kps[1, 9:])])
|
||||
return lower_boxes
|
||||
|
||||
|
||||
def open_annotations(path_ann):
|
||||
try:
|
||||
with open(path_ann, 'r') as f:
|
||||
annotations = json.load(f)
|
||||
except FileNotFoundError:
|
||||
annotations = []
|
||||
return annotations
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
import math
|
||||
import os
|
||||
import glob
|
||||
|
||||
@ -129,50 +128,6 @@ def split_training(names_gt, path_train, path_val):
|
||||
return set_train, set_val
|
||||
|
||||
|
||||
def parse_ground_truth(path_gt, category, spherical=False, verbose=False):
|
||||
"""Parse KITTI ground truth files"""
|
||||
from ..utils import correct_angle, to_spherical
|
||||
|
||||
boxes_gt = []
|
||||
ys = []
|
||||
truncs_gt = [] # Float from 0 to 1
|
||||
occs_gt = [] # Either 0,1,2,3 fully visible, partly occluded, largely occluded, unknown
|
||||
lines = []
|
||||
|
||||
with open(path_gt, "r") as f_gt:
|
||||
for line_gt in f_gt:
|
||||
line = line_gt.split()
|
||||
if check_conditions(line_gt, category, method='gt'):
|
||||
truncs_gt.append(float(line[1]))
|
||||
occs_gt.append(int(line[2]))
|
||||
boxes_gt.append([float(x) for x in line[4:8]])
|
||||
xyz = [float(x) for x in line[11:14]]
|
||||
hwl = [float(x) for x in line[8:11]]
|
||||
dd = float(math.sqrt(xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))
|
||||
yaw = float(line[14])
|
||||
assert - math.pi <= yaw <= math.pi
|
||||
alpha = float(line[3])
|
||||
sin, cos, yaw_corr = correct_angle(yaw, xyz)
|
||||
assert min(abs(-yaw_corr - alpha), (abs(yaw_corr - alpha))) < 0.15, "more than 10 degrees of error"
|
||||
if spherical:
|
||||
rtp = to_spherical(xyz)
|
||||
loc = rtp[1:3] + xyz[2:3] + rtp[0:1] # [theta, psi, z, r]
|
||||
else:
|
||||
loc = xyz + [dd]
|
||||
# cat = 0 if line[0] in ('Pedestrian', 'Person_sitting') else 1
|
||||
if line[0] in ('Pedestrian', 'Person_sitting'):
|
||||
cat = 0
|
||||
else:
|
||||
cat = 1
|
||||
output = loc + hwl + [sin, cos, yaw, cat]
|
||||
ys.append(output)
|
||||
if verbose:
|
||||
lines.append(line_gt)
|
||||
if verbose:
|
||||
return boxes_gt, ys, truncs_gt, occs_gt, lines
|
||||
return boxes_gt, ys, truncs_gt, occs_gt
|
||||
|
||||
|
||||
def factory_basename(dir_ann, dir_gt):
|
||||
""" Return all the basenames in the annotations folder corresponding to validation images"""
|
||||
|
||||
@ -191,64 +146,6 @@ def factory_basename(dir_ann, dir_gt):
|
||||
return set_val
|
||||
|
||||
|
||||
def factory_file(path_calib, dir_ann, basename, mode='left'):
|
||||
"""Choose the annotation and the calibration files. Stereo option with ite = 1"""
|
||||
|
||||
assert mode in ('left', 'right')
|
||||
p_left, p_right = get_calibration(path_calib)
|
||||
|
||||
if mode == 'left':
|
||||
kk, tt = p_left[:]
|
||||
path_ann = os.path.join(dir_ann, basename + '.png.predictions.json')
|
||||
|
||||
else:
|
||||
kk, tt = p_right[:]
|
||||
path_ann = os.path.join(dir_ann + '_right', basename + '.png.predictions.json')
|
||||
|
||||
from ..utils import open_annotations
|
||||
annotations = open_annotations(path_ann)
|
||||
|
||||
return annotations, kk, tt
|
||||
|
||||
|
||||
def get_category(keypoints, path_byc):
|
||||
"""Find the category for each of the keypoints"""
|
||||
|
||||
from ..utils import open_annotations
|
||||
dic_byc = open_annotations(path_byc)
|
||||
boxes_byc = dic_byc['boxes'] if dic_byc else []
|
||||
boxes_ped = make_lower_boxes(keypoints)
|
||||
|
||||
matches = get_matches_bikes(boxes_ped, boxes_byc)
|
||||
list_byc = [match[0] for match in matches]
|
||||
categories = [1.0 if idx in list_byc else 0.0 for idx, _ in enumerate(boxes_ped)]
|
||||
return categories
|
||||
|
||||
|
||||
def get_matches_bikes(boxes_ped, boxes_byc):
|
||||
from . import get_iou_matches_matrix
|
||||
matches = get_iou_matches_matrix(boxes_ped, boxes_byc, thresh=0.15)
|
||||
matches_b = []
|
||||
for idx, idx_byc in matches:
|
||||
box_ped = boxes_ped[idx]
|
||||
box_byc = boxes_byc[idx_byc]
|
||||
width_ped = box_ped[2] - box_ped[0]
|
||||
width_byc = box_byc[2] - box_byc[0]
|
||||
center_ped = (box_ped[2] + box_ped[0]) / 2
|
||||
center_byc = (box_byc[2] + box_byc[0]) / 2
|
||||
if abs(center_ped - center_byc) < min(width_ped, width_byc) / 4:
|
||||
matches_b.append((idx, idx_byc))
|
||||
return matches_b
|
||||
|
||||
|
||||
def make_lower_boxes(keypoints):
|
||||
lower_boxes = []
|
||||
keypoints = np.array(keypoints)
|
||||
for kps in keypoints:
|
||||
lower_boxes.append([min(kps[0, 9:]), min(kps[1, 9:]), max(kps[0, 9:]), max(kps[1, 9:])])
|
||||
return lower_boxes
|
||||
|
||||
|
||||
def read_and_rewrite(path_orig, path_new):
|
||||
"""Read and write same txt file. If file not found, create open file"""
|
||||
try:
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
|
||||
@ -44,15 +43,6 @@ def get_pixel_error(zz_gt):
|
||||
return error
|
||||
|
||||
|
||||
def open_annotations(path_ann):
|
||||
try:
|
||||
with open(path_ann, 'r') as f:
|
||||
annotations = json.load(f)
|
||||
except FileNotFoundError:
|
||||
annotations = []
|
||||
return annotations
|
||||
|
||||
|
||||
def make_new_directory(dir_out):
|
||||
"""Remove the output directory if already exists (avoid residual txt files)"""
|
||||
if os.path.exists(dir_out):
|
||||
|
||||
@ -12,7 +12,16 @@ D_MAX = BF / z_min
|
||||
|
||||
|
||||
def extract_stereo_matches(keypoint, keypoints_r, zz, phase='train', seed=0, method=None):
|
||||
"""Return binaries representing the match between the pose in the left and the ones in the right"""
|
||||
"""
|
||||
Return:
|
||||
1) a list of tuples that indicates, for a reference pose in the left image:
|
||||
- the index of the right pose
|
||||
- weather the right pose corresponds to the same person as the left pose (stereo match) or not
|
||||
For example: [(0,0), (1,0), (2,1)] means there are three right poses in the image
|
||||
and the third one is the same person as the reference pose
|
||||
2) a flag indicating whether a match has been found
|
||||
3) number of ambiguous instances, for which is not possible to define whether there is a correspondence
|
||||
"""
|
||||
|
||||
stereo_matches = []
|
||||
cnt_ambiguous = 0
|
||||
@ -70,20 +79,10 @@ def extract_stereo_matches(keypoint, keypoints_r, zz, phase='train', seed=0, met
|
||||
if idx_matches[num] not in used:
|
||||
stereo_matches.append((idx_matches[num], 0))
|
||||
|
||||
# elif len(stereo_matches) < 1:
|
||||
# stereo_matches.append((idx_match, 0))
|
||||
|
||||
# Easy-negative
|
||||
# elif len(idx_matches) > len(stereo_matches):
|
||||
# stereo_matches.append((idx_matches[-1], 0))
|
||||
# break # matches are ordered
|
||||
else:
|
||||
break
|
||||
used.append(idx_match)
|
||||
|
||||
# Make sure each left has at least a negative match
|
||||
# if not stereo_matches:
|
||||
# stereo_matches.append((idx_matches[0], 0))
|
||||
return stereo_matches, cnt_ambiguous
|
||||
|
||||
|
||||
@ -191,7 +190,7 @@ def verify_stereo(zz_stereo, zz_mono, disparity_x, disparity_y):
|
||||
y_max_difference = (80 / zz_mono)
|
||||
z_max_difference = 1 * zz_mono
|
||||
|
||||
cov = float(np.nanstd(disparity_x) / np.abs(np.nanmean(disparity_x))) # Coefficient of variation
|
||||
cov = float(np.nanstd(disparity_x) / np.abs(np.nanmean(disparity_x))) # pylint: disable=unused-variable
|
||||
avg_disparity_y = np.nanmedian(disparity_y)
|
||||
|
||||
return abs(zz_stereo - zz_mono) < z_max_difference and avg_disparity_y < y_max_difference and 1 < zz_stereo < 80
|
||||
|
||||
@ -7,6 +7,11 @@ import os
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Ellipse
|
||||
try:
|
||||
import pandas as pd
|
||||
DATAFRAME = pd.DataFrame
|
||||
except ImportError:
|
||||
DATAFRAME = None
|
||||
|
||||
from ..utils import get_task_error, get_pixel_error
|
||||
|
||||
@ -33,7 +38,7 @@ def show_results(dic_stats, clusters, net, dir_fig, show=False, save=False):
|
||||
excl_clusters = ['all', 'easy', 'moderate', 'hard', '49']
|
||||
clusters = [clst for clst in clusters if clst not in excl_clusters]
|
||||
styles = printing_styles(net)
|
||||
for idx_style, style in enumerate(styles.items()):
|
||||
for idx_style in styles:
|
||||
plt.figure(idx_style, figsize=FIGSIZE)
|
||||
plt.grid(linewidth=GRID_WIDTH)
|
||||
plt.xlim(x_min, x_max)
|
||||
@ -183,7 +188,6 @@ def show_method(save, dir_out='data/figures'):
|
||||
|
||||
|
||||
def show_box_plot(dic_errors, clusters, dir_fig, show=False, save=False):
|
||||
import pandas as pd
|
||||
excl_clusters = ['all', 'easy', 'moderate', 'hard']
|
||||
clusters = [int(clst) for clst in clusters if clst not in excl_clusters]
|
||||
methods = ('monstereo', 'pseudo-lidar', '3dop', 'monoloco')
|
||||
@ -192,7 +196,7 @@ def show_box_plot(dic_errors, clusters, dir_fig, show=False, save=False):
|
||||
xxs = get_distances(clusters)
|
||||
labels = [str(xx) for xx in xxs]
|
||||
for idx, method in enumerate(methods):
|
||||
df = pd.DataFrame([dic_errors[method][str(clst)] for clst in clusters[:-1]]).T
|
||||
df = DATAFRAME([dic_errors[method][str(clst)] for clst in clusters[:-1]]).T
|
||||
df.columns = labels
|
||||
|
||||
plt.figure(idx, figsize=FIGSIZE) # with 200 dpi it becomes 1920x1440
|
||||
@ -289,16 +293,16 @@ def expandgrid(*itrs):
|
||||
return combinations
|
||||
|
||||
|
||||
def get_percentile(dist_gmm):
|
||||
dd_gt = 1000
|
||||
mu_gmm = np.mean(dist_gmm)
|
||||
dist_d = dd_gt * mu_gmm / dist_gmm
|
||||
perc_d, _ = np.nanpercentile(dist_d, [18.5, 81.5]) # Laplace bi => 63%
|
||||
perc_d2, _ = np.nanpercentile(dist_d, [23, 77])
|
||||
mu_d = np.mean(dist_d)
|
||||
# mm_bi = (mu_d - perc_d) / mu_d
|
||||
# mm_test = (mu_d - perc_d2) / mu_d
|
||||
# mad_d = np.mean(np.abs(dist_d - mu_d))
|
||||
# def get_percentile(dist_gmm):
|
||||
# dd_gt = 1000
|
||||
# mu_gmm = np.mean(dist_gmm)
|
||||
# dist_d = dd_gt * mu_gmm / dist_gmm
|
||||
# perc_d, _ = np.nanpercentile(dist_d, [18.5, 81.5]) # Laplace bi => 63%
|
||||
# perc_d2, _ = np.nanpercentile(dist_d, [23, 77])
|
||||
# mu_d = np.mean(dist_d)
|
||||
# # mm_bi = (mu_d - perc_d) / mu_d
|
||||
# # mm_test = (mu_d - perc_d2) / mu_d
|
||||
# # mad_d = np.mean(np.abs(dist_d - mu_d))
|
||||
|
||||
|
||||
def printing_styles(net):
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
|
||||
# File adapted from https://github.com/vita-epfl/openpifpaf
|
||||
"""
|
||||
Adapted from https://github.com/openpifpaf,
|
||||
which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
and licensed under GNU AGPLv3
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import scipy.ndimage as ndimage
|
||||
except ImportError:
|
||||
matplotlib = None
|
||||
plt = None
|
||||
ndimage = None
|
||||
|
||||
|
||||
@ -69,7 +71,7 @@ def load_image(path, scale=1.0):
|
||||
return image
|
||||
|
||||
|
||||
class KeypointPainter(object):
|
||||
class KeypointPainter:
|
||||
def __init__(self, *,
|
||||
skeleton=None,
|
||||
xy_scale=1.0, highlight=None, highlight_invisible=False,
|
||||
|
||||
@ -10,11 +10,11 @@ def correct_boxes(boxes, hwls, xyzs, yaws, path_calib):
|
||||
p2_list = [float(xx) for xx in p2_str]
|
||||
P = np.array(p2_list).reshape(3, 4)
|
||||
boxes_new = []
|
||||
for idx, box in enumerate(boxes):
|
||||
for idx in range(boxes):
|
||||
hwl = hwls[idx]
|
||||
xyz = xyzs[idx]
|
||||
yaw = yaws[idx]
|
||||
corners_2d, corners_3d = compute_box_3d(hwl, xyz, yaw, P)
|
||||
corners_2d, _ = compute_box_3d(hwl, xyz, yaw, P)
|
||||
box_new = project_8p_to_4p(corners_2d).reshape(-1).tolist()
|
||||
boxes_new.append(box_new)
|
||||
return boxes_new
|
||||
@ -58,7 +58,6 @@ def compute_box_3d(hwl, xyz, ry, P):
|
||||
return corners_2d, np.transpose(corners_3d)
|
||||
|
||||
|
||||
|
||||
def roty(t):
|
||||
""" Rotation about the y-axis. """
|
||||
c = np.cos(t)
|
||||
@ -66,7 +65,6 @@ def roty(t):
|
||||
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
||||
|
||||
|
||||
|
||||
def project_to_image(pts_3d, P):
|
||||
""" Project 3d points to image plane.
|
||||
Usage: pts_2d = projectToImage(pts_3d, P)
|
||||
@ -87,7 +85,6 @@ def project_to_image(pts_3d, P):
|
||||
return pts_2d[:, 0:2]
|
||||
|
||||
|
||||
|
||||
def project_8p_to_4p(pts_2d):
|
||||
x0 = np.min(pts_2d[:, 0])
|
||||
x1 = np.max(pts_2d[:, 0])
|
||||
|
||||
38
pyproject.toml
Normal file
38
pyproject.toml
Normal file
@ -0,0 +1,38 @@
|
||||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["setuptools", "versioneer-518"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
testpaths = ["tests"]
|
||||
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
|
||||
|
||||
[tool.pylint.BASIC]
|
||||
class-const-naming-style = "snake_case" # checked since pylint 2.7.3
|
||||
|
||||
[tool.pylint.IMPORTS]
|
||||
allow-any-import-level = []
|
||||
|
||||
[tool.pylint.SIMILARITIES]
|
||||
min-similarity-lines = 15 # Minimum lines number of a similarity.
|
||||
ignore-comments = "yes" # Ignore comments when computing similarities.
|
||||
ignore-docstrings = "yes" # Ignore docstrings when computing similarities.
|
||||
ignore-imports = "yes" # Ignore imports when computing similarities.
|
||||
|
||||
[tool.pylint.TYPECHECK]
|
||||
generated-members = ["numpy.*", "torch.*", "cv2.*", "openpifpaf.functional.*"]
|
||||
ignored-modules = ["openpifpaf.functional"]
|
||||
disable = [
|
||||
"missing-docstring",
|
||||
"too-many-arguments",
|
||||
"too-many-instance-attributes",
|
||||
"too-many-locals",
|
||||
"too-few-public-methods",
|
||||
"unsubscriptable-object",
|
||||
"not-callable", # for torch tensors
|
||||
"invalid-name",
|
||||
"logging-format-interpolation",
|
||||
]
|
||||
[tool.pylint.FORMAT]
|
||||
max-line-length = 120
|
||||
|
||||
13
setup.cfg
Normal file
13
setup.cfg
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
[versioneer]
|
||||
VCS = git
|
||||
style = pep440
|
||||
versionfile_source = monoloco/_version.py
|
||||
versionfile_build = monoloco/_version.py
|
||||
tag_prefix = v
|
||||
#parentdir_prefix =
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length=120
|
||||
ignore=E731,E741,W503
|
||||
exclude=monoloco/__init__.py
|
||||
24
setup.py
24
setup.py
@ -1,13 +1,18 @@
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# extract version from __init__.py
|
||||
with open('monoloco/__init__.py', 'r') as f:
|
||||
VERSION_LINE = [l for l in f if l.startswith('__version__')][0]
|
||||
VERSION = VERSION_LINE.split('=')[1].strip()[1:-1]
|
||||
# This is needed for versioneer to be importable when building with PEP 517.
|
||||
# See <https://github.com/warner/python-versioneer/issues/193> and links
|
||||
# therein for more information.
|
||||
|
||||
import os, sys
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
import versioneer
|
||||
|
||||
setup(
|
||||
name='monoloco',
|
||||
version=VERSION,
|
||||
version=versioneer.get_version(),
|
||||
cmdclass=versioneer.get_cmdclass(),
|
||||
packages=[
|
||||
'monoloco',
|
||||
'monoloco.network',
|
||||
@ -29,15 +34,18 @@ setup(
|
||||
install_requires=[
|
||||
'openpifpaf>=v0.12.1',
|
||||
'matplotlib',
|
||||
'gdown',
|
||||
],
|
||||
extras_require={
|
||||
'test': [
|
||||
'pylint',
|
||||
'pytest',
|
||||
'gdown',
|
||||
'scipy', # for social distancing gaussian blur
|
||||
],
|
||||
'eval': [
|
||||
'tabulate',
|
||||
'sklearn',
|
||||
'pandas',
|
||||
'pylint',
|
||||
'pytest',
|
||||
],
|
||||
'prep': [
|
||||
'nuscenes-devkit==1.0.2',
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
tests/sample_joints-kitti-mono.json
Normal file
1
tests/sample_joints-kitti-mono.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/sample_joints-kitti-stereo.json
Normal file
1
tests/sample_joints-kitti-stereo.json
Normal file
File diff suppressed because one or more lines are too long
@ -1,89 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import math\n",
|
||||
"def calculate_iou(box1, box2):\n",
|
||||
"\n",
|
||||
" # Calculate the (x1, y1, x2, y2) coordinates of the intersection of box1 and box2. Calculate its Area.\n",
|
||||
" xi1 = max(box1[0], box2[0])\n",
|
||||
" yi1 = max(box1[1], box2[1])\n",
|
||||
" xi2 = min(box1[2], box2[2])\n",
|
||||
" yi2 = min(box1[3], box2[3])\n",
|
||||
" inter_area = max((xi2 - xi1), 0) * max((yi2 - yi1), 0) # Max keeps into account not overlapping box\n",
|
||||
"\n",
|
||||
" # Calculate the Union area by using Formula: Union(A,B) = A + B - Inter(A,B)\n",
|
||||
" box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])\n",
|
||||
" box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])\n",
|
||||
" union_area = box1_area + box2_area - inter_area\n",
|
||||
"\n",
|
||||
" # compute the IoU\n",
|
||||
" iou = inter_area / union_area\n",
|
||||
"\n",
|
||||
" return iou"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 64,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"15.0\n",
|
||||
"[8.450052369622647, 12.393410142113215, 88.45005236962265, 77.39341014211321]\n",
|
||||
"0.4850460596873889\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"x1 = 75\n",
|
||||
"y1 = 60\n",
|
||||
"\n",
|
||||
"box1 = [0, 0, x1, y1]\n",
|
||||
"alpha = math.atan2(110,75) # good number\n",
|
||||
"diag = 15\n",
|
||||
"x_cateto = diag * math.cos(alpha)\n",
|
||||
"y_cateto = diag * math.sin(alpha)\n",
|
||||
"print(math.sqrt(x_cateto**2 + y_cateto**2))\n",
|
||||
"box2 = [x_cateto, y_cateto, x1 + x_cateto + 5, y1 + y_cateto+ 5]\n",
|
||||
"print(box2)\n",
|
||||
"print(calculate_iou(box1, box2))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.7.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
"""Test if the main modules of the package run correctly"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Python does not consider the current directory to be a package
|
||||
sys.path.insert(0, os.path.join('..', 'monoloco'))
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from stereoloco.train import Trainer
|
||||
from stereoloco.network import MonoLoco
|
||||
from stereoloco.network.process import preprocess_pifpaf, factory_for_gt
|
||||
from stereoloco.visuals.printer import Printer
|
||||
|
||||
JOINTS = 'tests/joints_sample.json'
|
||||
PIFPAF_KEYPOINTS = 'tests/002282.png.pifpaf.json'
|
||||
IMAGE = 'docs/002282.png'
|
||||
|
||||
|
||||
def tst_trainer(joints):
|
||||
trainer = Trainer(joints=joints, epochs=150, lr=0.01)
|
||||
_ = trainer.train()
|
||||
dic_err, model = trainer.evaluate()
|
||||
return dic_err['val']['all']['mean'], model
|
||||
|
||||
|
||||
def tst_prediction(model, path_keypoints):
|
||||
with open(path_keypoints, 'r') as f:
|
||||
pifpaf_out = json.load(f)
|
||||
|
||||
kk, _ = factory_for_gt(im_size=[1240, 340])
|
||||
|
||||
# Preprocess pifpaf outputs and run monoloco
|
||||
boxes, keypoints = preprocess_pifpaf(pifpaf_out)
|
||||
monoloco = MonoLoco(model)
|
||||
outputs, varss = monoloco.forward(keypoints, kk)
|
||||
dic_out = monoloco.post_process(outputs, varss, boxes, keypoints, kk)
|
||||
return dic_out, kk
|
||||
|
||||
|
||||
def tst_printer(dic_out, kk, image_path):
|
||||
"""Draw a fake figure"""
|
||||
with open(image_path, 'rb') as f:
|
||||
pil_image = Image.open(f).convert('RGB')
|
||||
printer = Printer(image=pil_image, output_path='tests/test_image', kk=kk, output_types=['multi'], z_max=15)
|
||||
figures, axes = printer.factory_axes()
|
||||
printer.draw(figures, axes, dic_out, pil_image, save=True)
|
||||
|
||||
|
||||
def test_package():
|
||||
|
||||
# Training test
|
||||
val_acc, model = tst_trainer(JOINTS)
|
||||
assert val_acc < 2.5
|
||||
|
||||
# Prediction test
|
||||
dic_out, kk = tst_prediction(model, PIFPAF_KEYPOINTS)
|
||||
assert dic_out['boxes'] and kk
|
||||
|
||||
# Visualization test
|
||||
tst_printer(dic_out, kk, IMAGE)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
79
tests/test_train_mono.py
Normal file
79
tests/test_train_mono.py
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
"""
|
||||
Adapted from https://github.com/openpifpaf/openpifpaf/blob/main/tests/test_train.py,
|
||||
which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
and licensed under GNU AGPLv3
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import gdown
|
||||
|
||||
OPENPIFPAF_MODEL = 'https://drive.google.com/uc?id=1b408ockhh29OLAED8Tysd2yGZOo0N_SQ'
|
||||
|
||||
TRAIN_COMMAND = [
|
||||
'python3', '-m', 'monoloco.run',
|
||||
'train',
|
||||
'--joints', 'tests/sample_joints-kitti-mono.json',
|
||||
'--lr=0.001',
|
||||
'-e=10',
|
||||
]
|
||||
|
||||
|
||||
PREDICT_COMMAND = [
|
||||
'python3', '-m', 'monoloco.run',
|
||||
'predict',
|
||||
'docs/002282.png',
|
||||
'--output_types', 'multi', 'json',
|
||||
'--decoder-workers=0' # for windows
|
||||
]
|
||||
|
||||
PREDICT_COMMAND_SOCIAL_DISTANCE = [
|
||||
'python3', '-m', 'monoloco.run',
|
||||
'predict',
|
||||
'docs/frame0032.jpg',
|
||||
'--social_distance',
|
||||
'--output_types', 'front', 'bird',
|
||||
'--decoder-workers=0' # for windows'
|
||||
]
|
||||
|
||||
|
||||
def test_train_mono(tmp_path):
|
||||
# train a model
|
||||
train_cmd = TRAIN_COMMAND + ['--out={}'.format(os.path.join(tmp_path, 'train_test.pkl'))]
|
||||
print(' '.join(train_cmd))
|
||||
subprocess.run(train_cmd, check=True, capture_output=True)
|
||||
print(os.listdir(tmp_path))
|
||||
|
||||
# find the trained model checkpoint and download pifpaf one
|
||||
final_model = next(iter(f for f in os.listdir(tmp_path) if f.endswith('.pkl')))
|
||||
pifpaf_model = os.path.join(tmp_path, 'pifpaf_model.pkl')
|
||||
print('Downloading OpenPifPaf model in temporary folder')
|
||||
gdown.download(OPENPIFPAF_MODEL, pifpaf_model)
|
||||
|
||||
# run predictions with that model
|
||||
model = os.path.join(tmp_path, final_model)
|
||||
|
||||
print(model)
|
||||
predict_cmd = PREDICT_COMMAND + [
|
||||
'--model={}'.format(model),
|
||||
'--checkpoint={}'.format(pifpaf_model),
|
||||
'-o={}'.format(tmp_path),
|
||||
]
|
||||
print(' '.join(predict_cmd))
|
||||
subprocess.run(predict_cmd, check=True, capture_output=True)
|
||||
print(os.listdir(tmp_path))
|
||||
assert 'out_002282.png.multi.png' in os.listdir(tmp_path)
|
||||
assert 'out_002282.png.monoloco.json' in os.listdir(tmp_path)
|
||||
|
||||
predict_cmd_sd = PREDICT_COMMAND_SOCIAL_DISTANCE + [
|
||||
'--model={}'.format(model),
|
||||
'--checkpoint={}'.format(pifpaf_model),
|
||||
'-o={}'.format(tmp_path),
|
||||
]
|
||||
print(' '.join(predict_cmd_sd))
|
||||
subprocess.run(predict_cmd_sd, check=True, capture_output=True)
|
||||
print(os.listdir(tmp_path))
|
||||
assert 'out_frame0032.jpg.front.png' in os.listdir(tmp_path)
|
||||
assert 'out_frame0032.jpg.bird.png' in os.listdir(tmp_path)
|
||||
59
tests/test_train_stereo.py
Normal file
59
tests/test_train_stereo.py
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
"""
|
||||
Adapted from https://github.com/openpifpaf/openpifpaf/blob/main/tests/test_train.py,
|
||||
which is: 'Copyright 2019-2021 by Sven Kreiss and contributors. All rights reserved.'
|
||||
and licensed under GNU AGPLv3
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import gdown
|
||||
|
||||
OPENPIFPAF_MODEL = 'https://drive.google.com/uc?id=1b408ockhh29OLAED8Tysd2yGZOo0N_SQ'
|
||||
|
||||
TRAIN_COMMAND = [
|
||||
'python3', '-m', 'monoloco.run',
|
||||
'train',
|
||||
'--mode=stereo',
|
||||
'--joints', 'tests/sample_joints-kitti-stereo.json',
|
||||
'--lr=0.001',
|
||||
'-e=20',
|
||||
]
|
||||
|
||||
|
||||
PREDICT_COMMAND = [
|
||||
'python3', '-m', 'monoloco.run',
|
||||
'predict',
|
||||
'--mode=stereo',
|
||||
'--glob', 'docs/000840*.png',
|
||||
'--output_types', 'multi', 'json',
|
||||
'--decoder-workers=0', # for windows'
|
||||
]
|
||||
|
||||
|
||||
def test_train_stereo(tmp_path):
|
||||
# train a model
|
||||
train_cmd = TRAIN_COMMAND + ['--out={}'.format(os.path.join(tmp_path, 'train_test.pkl'))]
|
||||
print(' '.join(train_cmd))
|
||||
subprocess.run(train_cmd, check=True, capture_output=True)
|
||||
print(os.listdir(tmp_path))
|
||||
|
||||
# find the trained model checkpoint
|
||||
final_model = next(iter(f for f in os.listdir(tmp_path) if f.endswith('.pkl')))
|
||||
pifpaf_model = os.path.join(tmp_path, 'pifpaf_model.pkl')
|
||||
print('Downloading OpenPifPaf model in temporary folder')
|
||||
gdown.download(OPENPIFPAF_MODEL, pifpaf_model)
|
||||
|
||||
# run predictions with that model
|
||||
model = os.path.join(tmp_path, final_model)
|
||||
|
||||
predict_cmd = PREDICT_COMMAND + [
|
||||
'--model={}'.format(model),
|
||||
'--checkpoint={}'.format(pifpaf_model),
|
||||
'-o={}'.format(tmp_path),
|
||||
]
|
||||
print(' '.join(predict_cmd))
|
||||
subprocess.run(predict_cmd, check=True, capture_output=True)
|
||||
print(os.listdir(tmp_path))
|
||||
assert 'out_000840.png.multi.png' in os.listdir(tmp_path)
|
||||
@ -1,12 +1,14 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from monoloco.utils import pixel_to_camera
|
||||
from monoloco.utils import get_iou_matrix
|
||||
|
||||
# Python does not consider the current directory to be a package
|
||||
sys.path.insert(0, os.path.join('..', 'monoloco'))
|
||||
|
||||
|
||||
def test_iou():
|
||||
from stereoloco.utils import get_iou_matrix
|
||||
boxes_pred = [[1, 100, 1, 200]]
|
||||
boxes_gt = [[100., 120., 150., 160.],[12, 110, 130., 160.]]
|
||||
iou_matrix = get_iou_matrix(boxes_pred, boxes_gt)
|
||||
@ -14,7 +16,6 @@ def test_iou():
|
||||
|
||||
|
||||
def test_pixel_to_camera():
|
||||
from stereoloco.utils import pixel_to_camera
|
||||
kk = [[718.3351, 0., 600.3891], [0., 718.3351, 181.5122], [0., 0., 1.]]
|
||||
zz = 10
|
||||
uv_vector = [1000., 400.]
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# Python does not consider the current directory to be a package
|
||||
sys.path.insert(0, os.path.join('..', 'monoloco'))
|
||||
|
||||
|
||||
def test_printer():
|
||||
"""Draw a fake figure"""
|
||||
from stereoloco.visuals.printer import Printer
|
||||
test_list = [[718.3351, 0., 600.3891], [0., 718.3351, 181.5122], [0., 0., 1.]]
|
||||
boxes = [xx + [0] for xx in test_list]
|
||||
kk = test_list
|
||||
dict_ann = defaultdict(lambda: [1., 2., 3.], xyz_real=test_list, xyz_pred=test_list, uv_shoulders=test_list,
|
||||
boxes=boxes, boxes_gt=boxes)
|
||||
with open('docs/002282.png', 'rb') as f:
|
||||
pil_image = Image.open(f).convert('RGB')
|
||||
printer = Printer(image=pil_image, output_path=None, kk=kk, output_types=['combined'])
|
||||
figures, axes = printer.factory_axes()
|
||||
printer.draw(figures, axes, dict_ann, pil_image)
|
||||
1855
versioneer.py
Normal file
1855
versioneer.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user