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/
|
.idea/
|
||||||
data/
|
data
|
||||||
.DS_store
|
.DS_store
|
||||||
__pycache__
|
__pycache__
|
||||||
monoloco/*.pyc
|
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
|
This project and all its files are licensed under
|
||||||
GNU AGPLv3 or later version.
|
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.
|
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.
|
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
|
||||||
64
README.md
64
README.md
@ -1,9 +1,12 @@
|
|||||||
# 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" />
|
<img src="docs/monoloco.gif" alt="gif" />
|
||||||
|
|
||||||
|
|
||||||
This library is based on three research projects for monocular/stereo 3D human localization (detection), body orientation, and social distancing. Check the __video teaser__ of the library on [__YouTube__](https://www.youtube.com/watch?v=O5zhzi8mwJ4).
|
This library is based on three research projects for monocular/stereo 3D human localization (detection), body orientation, and social distancing. Check the __video teaser__ of the library on [__YouTube__](https://www.youtube.com/watch?v=O5zhzi8mwJ4).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -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:
|
For an example image, run the following command:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
python -m monoloco.run predict docs/002282.png \
|
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> \
|
-o <output directory> \
|
||||||
--long-edge <rescale the image by providing dimension of long side>
|
--long-edge <rescale the image by providing dimension of long side>
|
||||||
--n_dropout <50 to include epistemic uncertainty, 0 otherwise>
|
--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
|
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`.
|
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`
|
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.
|
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 structure
|
||||||
|
|
||||||
data
|
data
|
||||||
├── arrays
|
├── outputs
|
||||||
├── models
|
├── arrays
|
||||||
├── kitti
|
├── kitti
|
||||||
├── logs
|
|
||||||
├── output
|
|
||||||
|
|
||||||
Run the following inside monoloco repository:
|
Run the following inside monoloco repository:
|
||||||
```
|
```
|
||||||
mkdir data
|
mkdir data
|
||||||
cd data
|
cd data
|
||||||
mkdir arrays models kitti logs output
|
mkdir outputs arrays kitti
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Kitti Dataset
|
### 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 \
|
python -m openpifpaf.predict \
|
||||||
--glob "<kitti images directory>/*.png" \
|
--glob "data/kitti/images/*.png" \
|
||||||
--json-output <directory to contain predictions> \
|
--json-output <directory to contain predictions> \
|
||||||
--checkpoint=shufflenetv2k30 \
|
--checkpoint=shufflenetv2k30 \
|
||||||
--instance-threshold=0.05 --seed-threshold 0.05 --force-complete-pose
|
--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++:
|
For MonoLoco++:
|
||||||
```
|
```sh
|
||||||
python -m monoloco.run prep --dir_ann <directory that contains annotations>
|
python -m monoloco.run prep --dir_ann <directory that contains annotations>
|
||||||
```
|
```
|
||||||
|
|
||||||
For MonStereo:
|
For MonStereo:
|
||||||
```
|
```sh
|
||||||
python -m monoloco.run prep --mode stereo --dir_ann <directory that contains annotations>
|
python -m monoloco.run prep --mode stereo --dir_ann <directory that contains left annotations>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Collective Activity Dataset
|
### 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
|
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:
|
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 \
|
python -m monoloco.run eval \
|
||||||
--dir_ann <annotation directory> \
|
--dir_ann <annotation directory> \
|
||||||
--model <model path> \
|
--model #TODO \
|
||||||
--generate \
|
--generate \
|
||||||
--save \
|
--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)
|
### 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
|
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
|
# Draw keypoints and orientation
|
||||||
if 'front' in args.output_types:
|
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']
|
uv_centers = dic_out['uv_heads']
|
||||||
sizes = [abs(dic_out['uv_heads'][idx][1] - uv_s[1]) / 1.5 for idx, uv_s in
|
sizes = [abs(dic_out['uv_heads'][idx][1] - uv_s[1]) / 1.5 for idx, uv_s in
|
||||||
enumerate(dic_out['uv_shoulders'])]
|
enumerate(dic_out['uv_shoulders'])]
|
||||||
|
|||||||
@ -2,17 +2,23 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
import csv
|
import csv
|
||||||
|
import copy
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
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 ..prep import factory_file
|
||||||
from monoloco.network.process import factory_for_gt, preprocess_pifpaf
|
from ..network import Loco
|
||||||
from monoloco.activity import social_interactions
|
from ..network.process import factory_for_gt, preprocess_pifpaf
|
||||||
from monoloco.utils import open_annotations, get_iou_matches, get_difficulty
|
from ..activity import social_interactions
|
||||||
|
from ..utils import open_annotations, get_iou_matches, get_difficulty
|
||||||
|
|
||||||
|
|
||||||
class ActivityEvaluator:
|
class ActivityEvaluator:
|
||||||
@ -79,7 +85,7 @@ class ActivityEvaluator:
|
|||||||
im_size = image.size
|
im_size = image.size
|
||||||
assert len(im_size) > 1, "image with frame0001 not available"
|
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
|
# Collect PifPaf files and calibration
|
||||||
basename = os.path.basename(im_path)
|
basename = os.path.basename(im_path)
|
||||||
@ -101,14 +107,12 @@ class ActivityEvaluator:
|
|||||||
self.estimate_activity(dic_out, matches, ys_gt, categories=categories)
|
self.estimate_activity(dic_out, matches, ys_gt, categories=categories)
|
||||||
|
|
||||||
# Print Results
|
# 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}%")
|
print(f"Accuracy of category {seq}: {100*acc:.2f}%")
|
||||||
cout_results(self.cnt, self.all_gt, self.all_pred, categories=self.sequences)
|
cout_results(self.cnt, self.all_gt, self.all_pred, categories=self.sequences)
|
||||||
|
|
||||||
def eval_kitti(self):
|
def eval_kitti(self):
|
||||||
"""Parse KITTI Dataset and predict if people are talking or not"""
|
"""Parse KITTI Dataset and predict if people are talking or not"""
|
||||||
|
|
||||||
from ..utils import factory_file
|
|
||||||
files = glob.glob(self.dir_data + '/*.txt')
|
files = glob.glob(self.dir_data + '/*.txt')
|
||||||
# files = [self.dir_gt_kitti + '/001782.txt']
|
# files = [self.dir_gt_kitti + '/001782.txt']
|
||||||
assert files, "Empty directory"
|
assert files, "Empty directory"
|
||||||
@ -118,7 +122,7 @@ class ActivityEvaluator:
|
|||||||
# Collect PifPaf files and calibration
|
# Collect PifPaf files and calibration
|
||||||
basename, _ = os.path.splitext(os.path.basename(file))
|
basename, _ = os.path.splitext(os.path.basename(file))
|
||||||
path_calib = os.path.join(self.dir_kk, basename + '.txt')
|
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)
|
# Collect corresponding gt files (ys_gt: 1 or 0)
|
||||||
path_gt = os.path.join(self.dir_data, basename + '.txt')
|
path_gt = os.path.join(self.dir_data, basename + '.txt')
|
||||||
@ -149,7 +153,7 @@ class ActivityEvaluator:
|
|||||||
self.cnt['gt'][key] += 1
|
self.cnt['gt'][key] += 1
|
||||||
self.cnt['gt']['all'] += 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
|
# Select keys to update results for Collective or KITTI
|
||||||
keys = ('all', categories[idx_gt])
|
keys = ('all', categories[idx_gt])
|
||||||
@ -184,7 +188,7 @@ def parse_gt_collective(dir_data, seq, path_pif):
|
|||||||
with open(path, "r") as ff:
|
with open(path, "r") as ff:
|
||||||
reader = csv.reader(ff, delimiter='\t')
|
reader = csv.reader(ff, delimiter='\t')
|
||||||
dic_frames = defaultdict(lambda: defaultdict(list))
|
dic_frames = defaultdict(lambda: defaultdict(list))
|
||||||
for idx, line in enumerate(reader):
|
for line in reader:
|
||||||
box = convert_box(line[1:5])
|
box = convert_box(line[1:5])
|
||||||
cat = convert_category(line[5])
|
cat = convert_category(line[5])
|
||||||
dic_frames[line[0]]['boxes'].append(box)
|
dic_frames[line[0]]['boxes'].append(box)
|
||||||
|
|||||||
@ -7,13 +7,20 @@ Evaluate MonStereo code on KITTI dataset using ALE metric
|
|||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
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, \
|
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
|
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']
|
METHODS_STEREO = ['3dop', 'psf', 'pseudo-lidar', 'e2e', 'oc-stereo']
|
||||||
BASELINES = ['task_error', 'pixel_error']
|
BASELINES = ['task_error', 'pixel_error']
|
||||||
HEADERS = ('method', '<0.5', '<1m', '<2m', 'easy', 'moderate', 'hard', 'all')
|
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
|
methods = OUR_METHODS + METHODS_MONO + METHODS_STEREO
|
||||||
|
|
||||||
# Set directories
|
# Set directories
|
||||||
@ -39,8 +46,7 @@ class EvalKitti:
|
|||||||
path_val = os.path.join('splits', 'kitti_val.txt')
|
path_val = os.path.join('splits', 'kitti_val.txt')
|
||||||
dir_logs = os.path.join('data', 'logs')
|
dir_logs = os.path.join('data', 'logs')
|
||||||
assert os.path.exists(dir_logs), "No directory to save final statistics"
|
assert os.path.exists(dir_logs), "No directory to save final statistics"
|
||||||
dir_fig = os.path.join('data', 'figures')
|
dir_fig = os.path.join('figures', 'results')
|
||||||
assert os.path.exists(dir_logs), "No directory to save figures"
|
|
||||||
|
|
||||||
# Set thresholds to obtain comparable recalls
|
# Set thresholds to obtain comparable recalls
|
||||||
thresh_iou_monoloco = 0.3
|
thresh_iou_monoloco = 0.3
|
||||||
@ -49,9 +55,10 @@ class EvalKitti:
|
|||||||
thresh_conf_base = 0.5
|
thresh_conf_base = 0.5
|
||||||
|
|
||||||
def __init__(self, args):
|
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.verbose = args.verbose
|
||||||
self.net = args.net
|
|
||||||
self.save = args.save
|
self.save = args.save
|
||||||
self.show = args.show
|
self.show = args.show
|
||||||
|
|
||||||
@ -110,7 +117,7 @@ class EvalKitti:
|
|||||||
methods_out = defaultdict(tuple) # Save all methods for comparison
|
methods_out = defaultdict(tuple) # Save all methods for comparison
|
||||||
|
|
||||||
# Count ground_truth:
|
# 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):
|
for idx, box in enumerate(boxes_gt):
|
||||||
mode = get_difficulty(box, truncs_gt[idx], occs_gt[idx])
|
mode = get_difficulty(box, truncs_gt[idx], occs_gt[idx])
|
||||||
self.cnt_gt[mode] += 1
|
self.cnt_gt[mode] += 1
|
||||||
@ -144,10 +151,13 @@ class EvalKitti:
|
|||||||
self.show_statistics()
|
self.show_statistics()
|
||||||
|
|
||||||
def printer(self):
|
def printer(self):
|
||||||
|
if self.save:
|
||||||
|
os.makedirs(self.dir_fig, exist_ok=True)
|
||||||
if self.save or self.show:
|
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_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)
|
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)
|
show_box_plot(self.errors, self.CLUSTERS, self.dir_fig, show=self.show, save=self.save)
|
||||||
else:
|
else:
|
||||||
show_task_error(self.dir_fig, show=self.show, save=self.save)
|
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):
|
def _estimate_error(self, out_gt, out, method):
|
||||||
"""Estimate localization error"""
|
"""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:
|
if method in self.OUR_METHODS:
|
||||||
boxes, dds, cat, bis, epis = out
|
boxes, dds, cat, bis, epis = out
|
||||||
@ -363,7 +373,7 @@ class EvalKitti:
|
|||||||
for key in all_methods]
|
for key in all_methods]
|
||||||
|
|
||||||
results = [[key] + alp[idx] + ale[idx] for idx, key in enumerate(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')
|
print('-' * 90 + '\n')
|
||||||
|
|
||||||
def stats_height(self):
|
def stats_height(self):
|
||||||
@ -373,10 +383,8 @@ class EvalKitti:
|
|||||||
self.name = name
|
self.name = name
|
||||||
# Iterate over each line of the gt file and save box location and distances
|
# Iterate over each line of the gt file and save box location and distances
|
||||||
out_gt = parse_ground_truth(path_gt, 'pedestrian')
|
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 out_gt[1]:
|
||||||
for label in ys:
|
|
||||||
heights.append(label[4])
|
heights.append(label[4])
|
||||||
import numpy as np
|
|
||||||
tail1, tail2 = np.nanpercentile(np.array(heights), [5, 95])
|
tail1, tail2 = np.nanpercentile(np.array(heights), [5, 95])
|
||||||
print(average(heights))
|
print(average(heights))
|
||||||
print(len(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"""
|
"""Joints Analysis: Supplementary material of MonStereo"""
|
||||||
|
|
||||||
@ -11,26 +11,7 @@ import matplotlib.pyplot as plt
|
|||||||
|
|
||||||
from ..utils import find_cluster, average
|
from ..utils import find_cluster, average
|
||||||
from ..visuals.figures import get_distances
|
from ..visuals.figures import get_distances
|
||||||
|
from ..prep.transforms import COCO_KEYPOINTS
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def joints_variance(joints, clusters, dic_ms):
|
def joints_variance(joints, clusters, dic_ms):
|
||||||
@ -184,8 +165,8 @@ def variance_figures(dic_fin, clusters):
|
|||||||
plt.title("Standard deviation of joints disparity")
|
plt.title("Standard deviation of joints disparity")
|
||||||
yys_p = [el for _, el in dic_fin['pifpaf']['std_d'].items()]
|
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_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_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_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_p, marker='s', label="PifPaf")
|
||||||
plt.plot(xxs, yys_m, marker='o', label="Mask R-CNN")
|
plt.plot(xxs, yys_m, marker='o', label="Mask R-CNN")
|
||||||
# plt.plot(xxs, yys_p_z, marker='s', color='b', label="PifPaf (meters)")
|
# 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 import Loco
|
||||||
from ..network.process import preprocess_pifpaf
|
from ..network.process import preprocess_pifpaf
|
||||||
from ..network.geom_baseline import geometric_coordinates
|
from .geom_baseline import geometric_coordinates
|
||||||
from ..utils import get_keypoints, pixel_to_camera, factory_file, factory_basename, make_new_directory, get_category, \
|
from ..utils import get_keypoints, pixel_to_camera, factory_basename, make_new_directory, get_category, \
|
||||||
xyz_from_distance, read_and_rewrite
|
xyz_from_distance, read_and_rewrite
|
||||||
from .stereo_baselines import baselines_association
|
from .stereo_baselines import baselines_association
|
||||||
|
from ..prep import factory_file
|
||||||
from .reid_baseline import get_reid_features, ReID
|
from .reid_baseline import get_reid_features, ReID
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ class GenerateKitti:
|
|||||||
make_new_directory(di)
|
make_new_directory(di)
|
||||||
dir_out = {self.net: di}
|
dir_out = {self.net: di}
|
||||||
|
|
||||||
for mode, names in self.baselines.items():
|
for _, names in self.baselines.items():
|
||||||
for name in names:
|
for name in names:
|
||||||
di = os.path.join('data', 'kitti', name)
|
di = os.path.join('data', 'kitti', name)
|
||||||
make_new_directory(di)
|
make_new_directory(di)
|
||||||
@ -106,7 +107,7 @@ class GenerateKitti:
|
|||||||
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(1242, 374))
|
boxes, keypoints = preprocess_pifpaf(annotations, im_size=(1242, 374))
|
||||||
cat = get_category(keypoints, os.path.join(self.dir_byc, basename + '.json'))
|
cat = get_category(keypoints, os.path.join(self.dir_byc, basename + '.json'))
|
||||||
if keypoints:
|
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))
|
_, keypoints_r = preprocess_pifpaf(annotations_r, im_size=(1242, 374))
|
||||||
|
|
||||||
if self.net == 'monstereo':
|
if self.net == 'monstereo':
|
||||||
@ -121,7 +122,7 @@ class GenerateKitti:
|
|||||||
# Save txt files
|
# Save txt files
|
||||||
params = [kk, tt]
|
params = [kk, tt]
|
||||||
path_txt = os.path.join(dir_out[self.net], basename + '.txt')
|
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_ann += len(boxes)
|
||||||
cnt_file += 1
|
cnt_file += 1
|
||||||
|
|
||||||
@ -136,7 +137,7 @@ class GenerateKitti:
|
|||||||
# monocular baselines
|
# monocular baselines
|
||||||
for key in self.baselines['mono']:
|
for key in self.baselines['mono']:
|
||||||
path_txt = {key: os.path.join(dir_out[key], basename + '.txt')}
|
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
|
# stereo baselines
|
||||||
if self.baselines['stereo']:
|
if self.baselines['stereo']:
|
||||||
@ -149,7 +150,7 @@ class GenerateKitti:
|
|||||||
|
|
||||||
path_txt[key] = os.path.join(dir_out[key], basename + '.txt')
|
path_txt[key] = os.path.join(dir_out[key], basename + '.txt')
|
||||||
save_txts(path_txt[key], all_inputs[key], all_outputs[key], params,
|
save_txts(path_txt[key], all_inputs[key], all_outputs[key], params,
|
||||||
mode='baseline',
|
net='baseline',
|
||||||
cat=cat)
|
cat=cat)
|
||||||
|
|
||||||
print("\nSaved in {} txt {} annotations. Not found {} images".format(cnt_file, cnt_ann, cnt_no_file))
|
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):
|
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))
|
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
|
uv_centers = get_keypoints(keypoints, mode='bottom') # Kitti uses the bottom center to calculate depth
|
||||||
xy_centers = pixel_to_camera(uv_centers, kk, 1)
|
xy_centers = pixel_to_camera(uv_centers, kk, 1)
|
||||||
@ -198,15 +199,15 @@ class GenerateKitti:
|
|||||||
return dic_xyz
|
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[:]
|
xyzd, bis, epis, yaws, hs, ws, ls = all_outputs[:]
|
||||||
xyz = xyzd[:, 0:3]
|
xyz = xyzd[:, 0:3]
|
||||||
tt = [0, 0, 0]
|
tt = [0, 0, 0]
|
||||||
elif mode in ('monoloco', 'geometric'):
|
elif net in ('monoloco', 'geometric'):
|
||||||
tt = [0, 0, 0]
|
tt = [0, 0, 0]
|
||||||
dds, bis, epis, zzs_geom, xy_centers = all_outputs[:]
|
dds, bis, epis, zzs_geom, xy_centers = all_outputs[:]
|
||||||
xyz = xyz_from_distance(dds, xy_centers)
|
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]
|
yy = float(xyz[idx][1]) - tt[1]
|
||||||
zz = float(xyz[idx][2]) - tt[2]
|
zz = float(xyz[idx][2]) - tt[2]
|
||||||
|
|
||||||
if mode == 'geometric':
|
if net == 'geometric':
|
||||||
zz = zzs_geom[idx]
|
zz = zzs_geom[idx]
|
||||||
|
|
||||||
cam_0 = [xx, yy, zz]
|
cam_0 = [xx, yy, zz]
|
||||||
bi = float(bis[idx])
|
bi = float(bis[idx])
|
||||||
epi = float(epis[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])
|
alpha, ry = float(yaws[0][idx]), float(yaws[1][idx])
|
||||||
hwl = [float(hs[idx]), float(ws[idx]), float(ls[idx])]
|
hwl = [float(hs[idx]), float(ws[idx]), float(ls[idx])]
|
||||||
|
conf_scale = 0.035 # scale to obtain (approximately) same recall at evaluation
|
||||||
else:
|
else:
|
||||||
alpha, ry, hwl = -10., -10., [0, 0, 0]
|
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_scale = 0.05
|
||||||
conf = conf_scale * (uv_box[-1]) / (bi / math.sqrt(xx ** 2 + yy ** 2 + zz ** 2))
|
conf = conf_scale * (uv_box[-1]) / (bi / math.sqrt(xx ** 2 + yy ** 2 + zz ** 2))
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,34 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import math
|
import math
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import numpy as np
|
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
|
AVERAGE_Y = 0.48
|
||||||
CLUSTERS = ['10', '20', '30', 'all']
|
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):
|
def geometric_baseline(joints):
|
||||||
"""
|
"""
|
||||||
List of json files --> 2 lists with mean and std for each segment and the total count of instances
|
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']
|
'right_ankle']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
cnt_tot = 0
|
cnt_tot = 0
|
||||||
dic_dist = defaultdict(lambda: defaultdict(list))
|
dic_dist = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
@ -48,13 +63,13 @@ def geometric_baseline(joints):
|
|||||||
errors = calculate_error(dic_dist['error'])
|
errors = calculate_error(dic_dist['error'])
|
||||||
|
|
||||||
# Show results
|
# 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:
|
for key in dic_h_means:
|
||||||
logger.info("Average height of segment {} is {:.2f} with a std of {:.2f}".
|
print("Average height of segment {} is {:.2f} with a std of {:.2f}".
|
||||||
format(key, dic_h_means[key], dic_h_stds[key]))
|
format(key, dic_h_means[key], dic_h_stds[key]))
|
||||||
for clst in CLUSTERS:
|
for clst in CLUSTERS:
|
||||||
logger.info("Average error over the val set for clst {}: {:.2f}".format(clst, errors[clst]))
|
print("Average error over the val set for clst {}: {:.2f}".format(clst, errors[clst]))
|
||||||
logger.info("Joints used: {}".format(joints))
|
print("Joints used: {}".format(joints))
|
||||||
|
|
||||||
|
|
||||||
def update_distances(dic_fin, dic_dist, phase, average_y):
|
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])))
|
dy_met = abs(float((dic_xyz['hip'][0][1] - dic_xyz['shoulder'][0][1])))
|
||||||
|
|
||||||
# Estimate distance for a single annotation
|
# Estimate distance for a single annotation
|
||||||
z_met_real = compute_distance(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y,
|
z_met_real = compute_depth(dic_xyz_norm['shoulder'][0], dic_xyz_norm['hip'][0], average_y,
|
||||||
mode='real', dy_met=dy_met)
|
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_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
|
# 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_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
|
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
|
1. knowing specific height of the annotation (head-ankle) dy_met
|
||||||
2. using mean height of people (average_y)
|
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()
|
return features.cpu(), features_r.cpu()
|
||||||
|
|
||||||
|
|
||||||
class ReID(object):
|
class ReID:
|
||||||
def __init__(self, weights_path, device, num_classes=751, height=256, width=128):
|
def __init__(self, weights_path, device, num_classes=751, height=256, width=128):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
torch.manual_seed(1)
|
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
|
cnt_stereo['max'] = min(keypoints.shape[0], keypoints_r.shape[0]) # pylint: disable=E1136
|
||||||
|
|
||||||
# Filter joints disparity and calculate avg disparity
|
# 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
|
# Iterate over each left pose
|
||||||
for key in baselines:
|
for key in baselines:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import torch
|
|||||||
import torch.nn as nn
|
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'):
|
def __init__(self, input_size, output_size=2, linear_size=512, p_dropout=0.2, num_stage=3, device='cuda'):
|
||||||
super().__init__()
|
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
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import torch
|
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,\
|
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 ..activity import social_interactions
|
||||||
from .architectures import MonolocoModel, MonStereoModel
|
from .architectures import MonolocoModel, LocoModel
|
||||||
|
|
||||||
|
|
||||||
class Loco:
|
class Loco:
|
||||||
@ -69,7 +71,7 @@ class Loco:
|
|||||||
self.model = MonolocoModel(p_dropout=p_dropout, input_size=input_size, linear_size=linear_size,
|
self.model = MonolocoModel(p_dropout=p_dropout, input_size=input_size, linear_size=linear_size,
|
||||||
output_size=output_size)
|
output_size=output_size)
|
||||||
else:
|
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)
|
linear_size=linear_size, device=self.device)
|
||||||
|
|
||||||
self.model.load_state_dict(torch.load(model_path, map_location=lambda storage, loc: storage))
|
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 = self.model(inputs)
|
||||||
|
|
||||||
outputs = cluster_outputs(outputs, keypoints_r.shape[0])
|
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)
|
dic_out = extract_outputs(outputs_fin)
|
||||||
|
|
||||||
# For Median baseline
|
# For Median baseline
|
||||||
@ -136,7 +138,6 @@ class Loco:
|
|||||||
Apply dropout at test time to obtain combined aleatoric + epistemic uncertainty
|
Apply dropout at test time to obtain combined aleatoric + epistemic uncertainty
|
||||||
"""
|
"""
|
||||||
assert self.net in ('monoloco', 'monoloco_p', 'monoloco_pp'), "Not supported for MonStereo"
|
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
|
self.model.dropout.training = True # Manually reactivate dropout in eval
|
||||||
total_outputs = torch.empty((0, inputs.size()[0])).to(self.device)
|
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
|
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
|
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 = keypoints.cpu().numpy()
|
||||||
keypoints_r = keypoints_r.cpu().numpy()
|
keypoints_r = keypoints_r.cpu().numpy()
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
import torchvision
|
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)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -30,7 +30,7 @@ def preprocess_monstereo(keypoints, keypoints_r, kk):
|
|||||||
inputs_r = preprocess_monoloco(keypoints_r, kk)
|
inputs_r = preprocess_monoloco(keypoints_r, kk)
|
||||||
|
|
||||||
inputs = torch.empty((0, 68)).to(inputs_l.device)
|
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
|
clst = 0
|
||||||
# inp_l = torch.cat((inp_l, cat[:, idx:idx+1]), dim=1)
|
# inp_l = torch.cat((inp_l, cat[:, idx:idx+1]), dim=1)
|
||||||
for idx_r, inp_r in enumerate(inputs_r.split(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':
|
elif mode == 'right':
|
||||||
path_ann = os.path.join(dir_ann + '_right', basename + '.json')
|
path_ann = os.path.join(dir_ann + '_right', basename + '.json')
|
||||||
|
|
||||||
from ..utils import open_annotations
|
|
||||||
dic = open_annotations(path_ann)
|
dic = open_annotations(path_ann)
|
||||||
if isinstance(dic, list):
|
if isinstance(dic, list):
|
||||||
return [], []
|
return [], []
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
# pylint: disable=too-many-statements, too-many-branches, undefined-loop-variable
|
# 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 os
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@ -17,7 +20,11 @@ import openpifpaf
|
|||||||
import openpifpaf.datasets as datasets
|
import openpifpaf.datasets as datasets
|
||||||
from openpifpaf.predict import processor_factory, preprocess_factory
|
from openpifpaf.predict import processor_factory, preprocess_factory
|
||||||
from openpifpaf import decoder, network, visualizer, show, logger
|
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 .visuals.printer import Printer
|
||||||
from .network import Loco
|
from .network import Loco
|
||||||
from .network.process import factory_for_gt, preprocess_pifpaf
|
from .network.process import factory_for_gt, preprocess_pifpaf
|
||||||
@ -46,12 +53,15 @@ def get_torch_checkpoints_dir():
|
|||||||
|
|
||||||
def download_checkpoints(args):
|
def download_checkpoints(args):
|
||||||
torch_dir = get_torch_checkpoints_dir()
|
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}
|
dic_models = {'keypoints': pifpaf_model}
|
||||||
if not os.path.exists(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)
|
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':
|
if args.mode == 'keypoints':
|
||||||
return dic_models
|
return dic_models
|
||||||
@ -73,9 +83,9 @@ def download_checkpoints(args):
|
|||||||
model = os.path.join(torch_dir, name)
|
model = os.path.join(torch_dir, name)
|
||||||
dic_models[args.mode] = model
|
dic_models[args.mode] = model
|
||||||
if not os.path.exists(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)
|
LOG.info('Downloading model in %s', torch_dir)
|
||||||
gdown.download(path, model, quiet=False)
|
DOWNLOAD(path, model, quiet=False)
|
||||||
return dic_models
|
return dic_models
|
||||||
|
|
||||||
|
|
||||||
@ -209,7 +219,7 @@ def predict(args):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
LOG.info("Prediction with MonStereo")
|
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.forward(keypoints, kk, keypoints_r=keypoints_r)
|
||||||
dic_out = net.post_process(dic_out, boxes, keypoints, kk, dic_gt)
|
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
|
# Verify conflicting options
|
||||||
if 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'])):
|
||||||
assert args.mode != 'keypoints', "for keypoints please use pifpaf original arguments"
|
assert args.mode != 'keypoints', "for keypoints please use pifpaf original arguments"
|
||||||
if args.social_distance:
|
else:
|
||||||
assert args.mode == 'mono', "Social distancing only works with monocular network"
|
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':
|
if args.mode == 'keypoints':
|
||||||
annotation_painter = openpifpaf.show.AnnotationPainter()
|
annotation_painter = openpifpaf.show.AnnotationPainter()
|
||||||
with openpifpaf.show.image_canvas(pifpaf_outs['image'], output_path) as ax:
|
with openpifpaf.show.image_canvas(pifpaf_outs['image'], output_path) as ax:
|
||||||
annotation_painter.annotations(ax, pifpaf_outs['pred'])
|
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)
|
LOG.info(output_path)
|
||||||
if args.social_distance:
|
if args.social_distance:
|
||||||
show_social(args, pifpaf_outs['image'], output_path, pifpaf_outs['left'], dic_out)
|
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)
|
figures, axes = printer.factory_axes(dic_out)
|
||||||
printer.draw(figures, axes, pifpaf_outs['image'])
|
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:
|
with open(os.path.join(output_path + '.monoloco.json'), 'w') as ff:
|
||||||
json.dump(dic_out, 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 = []
|
ys = []
|
||||||
|
|
||||||
for box_obj in boxes_obj:
|
for box_obj in boxes_obj:
|
||||||
|
|
||||||
# Select category
|
# Select category
|
||||||
if box_obj.name[:6] != 'animal':
|
if box_obj.name[:6] != 'animal':
|
||||||
general_name = box_obj.name.split('.')[0] + '.' + box_obj.name.split('.')[1]
|
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])
|
all_inputs.extend(inputs[idx])
|
||||||
|
|
||||||
indices_idx = [idx for (idx, idx_gt) in matches]
|
indices_idx = [idx for (idx, idx_gt) in matches]
|
||||||
if len(sorted_indices) > 2:
|
|
||||||
aa = 5
|
|
||||||
for ii in range(1, 3):
|
for ii in range(1, 3):
|
||||||
try:
|
try:
|
||||||
index = sorted_indices[ii]
|
index = sorted_indices[ii]
|
||||||
@ -266,17 +263,3 @@ def extract_social(inputs, ys, keypoints, idx, matches):
|
|||||||
all_inputs.extend([0.] * 2)
|
all_inputs.extend([0.] * 2)
|
||||||
assert len(all_inputs) == 34 + 2 * 2
|
assert len(all_inputs) == 34 + 2 * 2
|
||||||
return all_inputs
|
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
|
import math
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from ..utils import correct_angle, to_cartesian, to_spherical
|
||||||
|
|
||||||
BASELINE = 0.54
|
BASELINE = 0.54
|
||||||
BF = BASELINE * 721
|
BF = BASELINE * 721
|
||||||
|
|
||||||
@ -75,7 +78,6 @@ def flip_inputs(keypoints, im_w, mode=None):
|
|||||||
|
|
||||||
def flip_labels(boxes_gt, labels, im_w):
|
def flip_labels(boxes_gt, labels, im_w):
|
||||||
"""Correct x, d positions and angles after horizontal flipping"""
|
"""Correct x, d positions and angles after horizontal flipping"""
|
||||||
from ..utils import correct_angle, to_cartesian, to_spherical
|
|
||||||
boxes_flip = deepcopy(boxes_gt)
|
boxes_flip = deepcopy(boxes_gt)
|
||||||
labels_flip = deepcopy(labels)
|
labels_flip = deepcopy(labels)
|
||||||
|
|
||||||
@ -98,29 +100,28 @@ def flip_labels(boxes_gt, labels, im_w):
|
|||||||
yaw = label_flip[9]
|
yaw = label_flip[9]
|
||||||
yaw_n = math.copysign(1, yaw) * (np.pi - abs(yaw)) # Horizontal flipping change of angle
|
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
|
label_flip[7], label_flip[8], label_flip[9] = sin, cos, yaw_n
|
||||||
|
|
||||||
return boxes_flip, labels_flip
|
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 label_s[-1] > 0.9 else 1
|
||||||
n_labels = 3 if s_match > 0.9 else 1
|
|
||||||
height_min = 1.2
|
height_min = 1.2
|
||||||
height_max = 2
|
height_max = 2
|
||||||
av_height = 1.71
|
av_height = 1.71
|
||||||
kps_aug = [[kps.clone(), kps_r.clone()] for _ in range(n_labels+1)]
|
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)
|
np.random.seed(seed)
|
||||||
heights = np.random.uniform(height_min, height_max, n_labels) # 3 samples
|
heights = np.random.uniform(height_min, height_max, n_labels) # 3 samples
|
||||||
zzs = heights * label[2] / av_height
|
zzs = heights * label_s[2] / av_height
|
||||||
disp = BF / label[2]
|
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)
|
xyz = to_cartesian(rtp)
|
||||||
|
|
||||||
for i in range(n_labels):
|
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
|
import argparse
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ def cli():
|
|||||||
# Predict (2D pose and/or 3D location from images)
|
# Predict (2D pose and/or 3D location from images)
|
||||||
predict_parser.add_argument('images', nargs='*', help='input 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('--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('-o', '--output-directory', help='Output directory')
|
||||||
predict_parser.add_argument('--output_types', nargs='+', default=['json'],
|
predict_parser.add_argument('--output_types', nargs='+', default=['json'],
|
||||||
help='what to output: json keypoints skeleton for Pifpaf'
|
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('--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('--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('--disable-cuda', action='store_true', help='disable CUDA')
|
||||||
predict_parser.add_argument('--focal',
|
predict_parser.add_argument('--precise-rescaling', dest='fast_rescaling', default=True, action='store_false',
|
||||||
help='focal length in mm for a sensor size of 7.2x5.4 mm. Default nuScenes sensor',
|
help='use more exact image rescaling (requires scipy)')
|
||||||
type=float, default=5.7)
|
predict_parser.add_argument('--decoder-workers', default=None, type=int,
|
||||||
|
help='number of workers for pose decoding, 0 for windows')
|
||||||
|
|
||||||
decoder.cli(parser)
|
decoder.cli(parser)
|
||||||
logger.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('--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('--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('--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
|
# Social distancing and social interactions
|
||||||
predict_parser.add_argument('--social_distance', help='social', action='store_true')
|
predict_parser.add_argument('--social_distance', help='social', action='store_true')
|
||||||
@ -74,11 +78,12 @@ def cli():
|
|||||||
# Training
|
# Training
|
||||||
training_parser.add_argument('--joints', help='Json file with input joints', required=True)
|
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('--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('-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('--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('--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('--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_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('--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)
|
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 = PreprocessNuscenes(args.dir_ann, args.dir_nuscenes, args.dataset, args.iou_min)
|
||||||
prep.run()
|
prep.run()
|
||||||
else:
|
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)
|
prep = PreprocessKitti(args.dir_ann, mode=args.mode, iou_min=args.iou_min)
|
||||||
if args.activity:
|
if args.activity:
|
||||||
prep.prep_activity()
|
prep.process_activity()
|
||||||
else:
|
else:
|
||||||
prep.run()
|
prep.run()
|
||||||
|
|
||||||
@ -162,7 +167,7 @@ def main():
|
|||||||
|
|
||||||
elif args.geometric:
|
elif args.geometric:
|
||||||
assert args.joints, "joints argument not provided"
|
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)
|
geometric_baseline(args.joints)
|
||||||
|
|
||||||
elif args.variance:
|
elif args.variance:
|
||||||
|
|||||||
@ -60,6 +60,7 @@ class KeypointsDataset(Dataset):
|
|||||||
self.outputs_all = torch.tensor(dic_jo[phase]['Y'])
|
self.outputs_all = torch.tensor(dic_jo[phase]['Y'])
|
||||||
self.names_all = dic_jo[phase]['names']
|
self.names_all = dic_jo[phase]['names']
|
||||||
self.kps_all = torch.tensor(dic_jo[phase]['kps'])
|
self.kps_all = torch.tensor(dic_jo[phase]['kps'])
|
||||||
|
self.version = dic_jo['version']
|
||||||
|
|
||||||
# Extract annotations divided in clusters
|
# Extract annotations divided in clusters
|
||||||
self.dic_clst = dic_jo[phase]['clst']
|
self.dic_clst = dic_jo[phase]['clst']
|
||||||
@ -90,3 +91,6 @@ class KeypointsDataset(Dataset):
|
|||||||
count = len(self.dic_clst[clst]['Y'])
|
count = len(self.dic_clst[clst]['Y'])
|
||||||
|
|
||||||
return inputs, outputs, count
|
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 math
|
||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from ..network import extract_labels, extract_labels_aux, extract_outputs
|
from ..network import extract_labels, extract_labels_aux, extract_outputs
|
||||||
|
|
||||||
@ -180,6 +186,58 @@ class GaussianLoss(torch.nn.Module):
|
|||||||
return torch.sum(values)
|
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):
|
def angle_loss(orient, gt_orient):
|
||||||
"""Only for evaluation"""
|
"""Only for evaluation"""
|
||||||
angles = torch.atan2(orient[:, 0], orient[:, 1])
|
angles = torch.atan2(orient[:, 0], orient[:, 1])
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
# pylint: disable=too-many-statements
|
# pylint: disable=too-many-statements
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Training and evaluation of a neural network which predicts 3D localization and confidence intervals
|
Training and evaluation of a neural network that, given 2D joints, estimates:
|
||||||
given 2d joints
|
- 3D localization and confidence intervals
|
||||||
|
- Orientation
|
||||||
|
- Bounding box dimensions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
@ -12,7 +14,6 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import warnings
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
@ -20,10 +21,11 @@ import torch
|
|||||||
from torch.utils.data import DataLoader
|
from torch.utils.data import DataLoader
|
||||||
from torch.optim import lr_scheduler
|
from torch.optim import lr_scheduler
|
||||||
|
|
||||||
|
from .. import __version__
|
||||||
from .datasets import KeypointsDataset
|
from .datasets import KeypointsDataset
|
||||||
from .losses import CompositeLoss, MultiTaskLoss, AutoTuneMultiTaskLoss
|
from .losses import CompositeLoss, MultiTaskLoss, AutoTuneMultiTaskLoss
|
||||||
from ..network import extract_outputs, extract_labels
|
from ..network import extract_outputs, extract_labels
|
||||||
from ..network.architectures import MonStereoModel
|
from ..network.architectures import LocoModel
|
||||||
from ..utils import set_logger
|
from ..utils import set_logger
|
||||||
|
|
||||||
|
|
||||||
@ -35,21 +37,16 @@ class Trainer:
|
|||||||
val_task = 'd'
|
val_task = 'd'
|
||||||
lambdas = (1, 1, 1, 1, 1, 1, 1, 1)
|
lambdas = (1, 1, 1, 1, 1, 1, 1, 1)
|
||||||
clusters = ['10', '20', '30', '40']
|
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):
|
def __init__(self, args):
|
||||||
"""
|
"""
|
||||||
Initialize directories, load the data and parameters for the training
|
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"
|
assert os.path.exists(args.joints), "Input file not found"
|
||||||
|
|
||||||
self.mode = args.mode
|
self.mode = args.mode
|
||||||
self.joints = args.joints
|
self.joints = args.joints
|
||||||
self.num_epochs = args.epochs
|
self.num_epochs = args.epochs
|
||||||
@ -60,10 +57,22 @@ class Trainer:
|
|||||||
self.sched_gamma = args.sched_gamma
|
self.sched_gamma = args.sched_gamma
|
||||||
self.hidden_size = args.hidden_size
|
self.hidden_size = args.hidden_size
|
||||||
self.n_stage = args.n_stage
|
self.n_stage = args.n_stage
|
||||||
self.dir_out = dir_out
|
|
||||||
self.r_seed = args.r_seed
|
self.r_seed = args.r_seed
|
||||||
self.auto_tune_mtl = args.auto_tune_mtl
|
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
|
# Select the device
|
||||||
use_cuda = torch.cuda.is_available()
|
use_cuda = torch.cuda.is_available()
|
||||||
self.device = torch.device("cuda" if use_cuda else "cpu")
|
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 = MultiTaskLoss(losses_tr, losses_val, self.lambdas, self.tasks)
|
||||||
self.mt_loss.to(self.device)
|
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
|
# Dataloader
|
||||||
self.dataloaders = {phase: DataLoader(KeypointsDataset(self.joints, phase=phase),
|
self.dataloaders = {phase: DataLoader(KeypointsDataset(self.joints, phase=phase),
|
||||||
batch_size=args.bs, shuffle=True) for phase in ['train', 'val']}
|
batch_size=args.bs, shuffle=True) for phase in ['train', 'val']}
|
||||||
|
|
||||||
self.dataset_sizes = {phase: len(KeypointsDataset(self.joints, phase=phase))
|
self.dataset_sizes = {phase: len(KeypointsDataset(self.joints, phase=phase))
|
||||||
for phase in ['train', 'val']}
|
for phase in ['train', 'val']}
|
||||||
|
self.dataset_version = KeypointsDataset(self.joints, phase='train').get_version()
|
||||||
|
|
||||||
|
self._set_logger(args)
|
||||||
|
|
||||||
# Define the model
|
# Define the model
|
||||||
self.logger.info('Sizes of the dataset: {}'.format(self.dataset_sizes))
|
self.logger.info('Sizes of the dataset: {}'.format(self.dataset_sizes))
|
||||||
print(">>> creating model")
|
print(">>> creating model")
|
||||||
|
|
||||||
self.model = MonStereoModel(input_size=input_size, output_size=output_size, linear_size=args.hidden_size,
|
self.model = LocoModel(
|
||||||
p_dropout=args.dropout, num_stage=self.n_stage, device=self.device)
|
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)
|
self.model.to(self.device)
|
||||||
print(">>> model params: {:.3f}M".format(sum(p.numel() for p in self.model.parameters()) / 1000000.0))
|
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())))
|
print(">>> loss params: {}".format(sum(p.numel() for p in self.mt_loss.parameters())))
|
||||||
@ -158,7 +150,7 @@ class Trainer:
|
|||||||
if phase == 'train':
|
if phase == 'train':
|
||||||
self.optimizer.zero_grad()
|
self.optimizer.zero_grad()
|
||||||
outputs = self.model(inputs)
|
outputs = self.model(inputs)
|
||||||
loss, loss_values = self.mt_loss(outputs, labels, phase=phase)
|
loss, _ = self.mt_loss(outputs, labels, phase=phase)
|
||||||
loss.backward()
|
loss.backward()
|
||||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 3)
|
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 3)
|
||||||
self.optimizer.step()
|
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('Best validation Accuracy for {}: {:.3f}'.format(self.val_task, best_acc))
|
||||||
self.logger.info('Saved weights of the model at epoch: {}'.format(best_epoch))
|
self.logger.info('Saved weights of the model at epoch: {}'.format(best_epoch))
|
||||||
|
|
||||||
if self.print_loss:
|
self._print_losses(epoch_losses)
|
||||||
print_losses(epoch_losses)
|
|
||||||
|
|
||||||
# load best model weights
|
# load best model weights
|
||||||
self.model.load_state_dict(best_model_wts)
|
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):
|
def compute_stats(self, outputs, labels, dic_err, size_eval, clst):
|
||||||
"""Compute mean, bi and max of torch tensors"""
|
"""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
|
rel_frac = outputs.size(0) / size_eval
|
||||||
|
|
||||||
tasks = self.tasks[:-1] if self.tasks[-1] == 'aux' else self.tasks # Exclude auxiliary
|
tasks = self.tasks[:-1] if self.tasks[-1] == 'aux' else self.tasks # Exclude auxiliary
|
||||||
@ -333,6 +324,41 @@ class Trainer:
|
|||||||
if epoch % 10 == 0:
|
if epoch % 10 == 0:
|
||||||
print(string.format(*format_list))
|
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):
|
def debug_plots(inputs, labels):
|
||||||
inputs_shoulder = inputs.cpu().numpy()[:, 5]
|
inputs_shoulder = inputs.cpu().numpy()[:, 5]
|
||||||
@ -347,15 +373,6 @@ def debug_plots(inputs, labels):
|
|||||||
plt.show()
|
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):
|
def get_accuracy(outputs, labels):
|
||||||
"""From Binary cross entropy outputs to accuracy"""
|
"""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 .iou import get_iou_matches, reorder_matches, get_iou_matrix, get_iou_matches_matrix, get_category, \
|
||||||
from .misc import get_task_error, get_pixel_error, append_cluster, open_annotations, make_new_directory,\
|
open_annotations
|
||||||
|
from .misc import get_task_error, get_pixel_error, append_cluster, make_new_directory,\
|
||||||
normalize_hwl, average
|
normalize_hwl, average
|
||||||
from .kitti import check_conditions, get_difficulty, split_training, parse_ground_truth, get_calibration, \
|
from .kitti import check_conditions, get_difficulty, split_training, get_calibration, \
|
||||||
factory_basename, factory_file, get_category, read_and_rewrite, find_cluster
|
factory_basename, read_and_rewrite, find_cluster
|
||||||
from .camera import xyz_from_distance, get_keypoints, pixel_to_camera, project_3d, open_image, correct_angle,\
|
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
|
to_spherical, to_cartesian, back_correct_angles, project_to_pixels
|
||||||
from .logs import set_logger
|
from .logs import set_logger
|
||||||
from ..utils.nuscenes import select_categories
|
from .nuscenes import select_categories
|
||||||
from ..utils.stereo import mask_joint_disparity, average_locations, extract_stereo_matches, \
|
from .stereo import mask_joint_disparity, average_locations, extract_stereo_matches, \
|
||||||
verify_stereo, disparity_to_depth
|
verify_stereo, disparity_to_depth
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ def get_iou_matches(boxes, boxes_gt, iou_min=0.3):
|
|||||||
for idx in indices[::-1]:
|
for idx in indices[::-1]:
|
||||||
box = boxes[idx]
|
box = boxes[idx]
|
||||||
ious = []
|
ious = []
|
||||||
for idx_gt, box_gt in enumerate(boxes_gt):
|
for box_gt in boxes_gt:
|
||||||
iou = calculate_iou(box, box_gt)
|
iou = calculate_iou(box, box_gt)
|
||||||
ious.append(iou)
|
ious.append(iou)
|
||||||
idx_gt_max = int(np.argmax(ious))
|
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]
|
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]
|
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 os
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
@ -129,50 +128,6 @@ def split_training(names_gt, path_train, path_val):
|
|||||||
return set_train, set_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):
|
def factory_basename(dir_ann, dir_gt):
|
||||||
""" Return all the basenames in the annotations folder corresponding to validation images"""
|
""" 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
|
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):
|
def read_and_rewrite(path_orig, path_new):
|
||||||
"""Read and write same txt file. If file not found, create open file"""
|
"""Read and write same txt file. If file not found, create open file"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -44,15 +43,6 @@ def get_pixel_error(zz_gt):
|
|||||||
return error
|
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):
|
def make_new_directory(dir_out):
|
||||||
"""Remove the output directory if already exists (avoid residual txt files)"""
|
"""Remove the output directory if already exists (avoid residual txt files)"""
|
||||||
if os.path.exists(dir_out):
|
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):
|
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 = []
|
stereo_matches = []
|
||||||
cnt_ambiguous = 0
|
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:
|
if idx_matches[num] not in used:
|
||||||
stereo_matches.append((idx_matches[num], 0))
|
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:
|
else:
|
||||||
break
|
break
|
||||||
used.append(idx_match)
|
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
|
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)
|
y_max_difference = (80 / zz_mono)
|
||||||
z_max_difference = 1 * 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)
|
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
|
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 numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.patches import Ellipse
|
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
|
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']
|
excl_clusters = ['all', 'easy', 'moderate', 'hard', '49']
|
||||||
clusters = [clst for clst in clusters if clst not in excl_clusters]
|
clusters = [clst for clst in clusters if clst not in excl_clusters]
|
||||||
styles = printing_styles(net)
|
styles = printing_styles(net)
|
||||||
for idx_style, style in enumerate(styles.items()):
|
for idx_style in styles:
|
||||||
plt.figure(idx_style, figsize=FIGSIZE)
|
plt.figure(idx_style, figsize=FIGSIZE)
|
||||||
plt.grid(linewidth=GRID_WIDTH)
|
plt.grid(linewidth=GRID_WIDTH)
|
||||||
plt.xlim(x_min, x_max)
|
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):
|
def show_box_plot(dic_errors, clusters, dir_fig, show=False, save=False):
|
||||||
import pandas as pd
|
|
||||||
excl_clusters = ['all', 'easy', 'moderate', 'hard']
|
excl_clusters = ['all', 'easy', 'moderate', 'hard']
|
||||||
clusters = [int(clst) for clst in clusters if clst not in excl_clusters]
|
clusters = [int(clst) for clst in clusters if clst not in excl_clusters]
|
||||||
methods = ('monstereo', 'pseudo-lidar', '3dop', 'monoloco')
|
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)
|
xxs = get_distances(clusters)
|
||||||
labels = [str(xx) for xx in xxs]
|
labels = [str(xx) for xx in xxs]
|
||||||
for idx, method in enumerate(methods):
|
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
|
df.columns = labels
|
||||||
|
|
||||||
plt.figure(idx, figsize=FIGSIZE) # with 200 dpi it becomes 1920x1440
|
plt.figure(idx, figsize=FIGSIZE) # with 200 dpi it becomes 1920x1440
|
||||||
@ -289,16 +293,16 @@ def expandgrid(*itrs):
|
|||||||
return combinations
|
return combinations
|
||||||
|
|
||||||
|
|
||||||
def get_percentile(dist_gmm):
|
# def get_percentile(dist_gmm):
|
||||||
dd_gt = 1000
|
# dd_gt = 1000
|
||||||
mu_gmm = np.mean(dist_gmm)
|
# mu_gmm = np.mean(dist_gmm)
|
||||||
dist_d = dd_gt * mu_gmm / dist_gmm
|
# dist_d = dd_gt * mu_gmm / dist_gmm
|
||||||
perc_d, _ = np.nanpercentile(dist_d, [18.5, 81.5]) # Laplace bi => 63%
|
# perc_d, _ = np.nanpercentile(dist_d, [18.5, 81.5]) # Laplace bi => 63%
|
||||||
perc_d2, _ = np.nanpercentile(dist_d, [23, 77])
|
# perc_d2, _ = np.nanpercentile(dist_d, [23, 77])
|
||||||
mu_d = np.mean(dist_d)
|
# mu_d = np.mean(dist_d)
|
||||||
# mm_bi = (mu_d - perc_d) / mu_d
|
# # mm_bi = (mu_d - perc_d) / mu_d
|
||||||
# mm_test = (mu_d - perc_d2) / mu_d
|
# # mm_test = (mu_d - perc_d2) / mu_d
|
||||||
# mad_d = np.mean(np.abs(dist_d - mu_d))
|
# # mad_d = np.mean(np.abs(dist_d - mu_d))
|
||||||
|
|
||||||
|
|
||||||
def printing_styles(net):
|
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
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import scipy.ndimage as ndimage
|
import scipy.ndimage as ndimage
|
||||||
except ImportError:
|
except ImportError:
|
||||||
matplotlib = None
|
|
||||||
plt = None
|
|
||||||
ndimage = None
|
ndimage = None
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ def load_image(path, scale=1.0):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
class KeypointPainter(object):
|
class KeypointPainter:
|
||||||
def __init__(self, *,
|
def __init__(self, *,
|
||||||
skeleton=None,
|
skeleton=None,
|
||||||
xy_scale=1.0, highlight=None, highlight_invisible=False,
|
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]
|
p2_list = [float(xx) for xx in p2_str]
|
||||||
P = np.array(p2_list).reshape(3, 4)
|
P = np.array(p2_list).reshape(3, 4)
|
||||||
boxes_new = []
|
boxes_new = []
|
||||||
for idx, box in enumerate(boxes):
|
for idx in range(boxes):
|
||||||
hwl = hwls[idx]
|
hwl = hwls[idx]
|
||||||
xyz = xyzs[idx]
|
xyz = xyzs[idx]
|
||||||
yaw = yaws[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()
|
box_new = project_8p_to_4p(corners_2d).reshape(-1).tolist()
|
||||||
boxes_new.append(box_new)
|
boxes_new.append(box_new)
|
||||||
return boxes_new
|
return boxes_new
|
||||||
@ -58,7 +58,6 @@ def compute_box_3d(hwl, xyz, ry, P):
|
|||||||
return corners_2d, np.transpose(corners_3d)
|
return corners_2d, np.transpose(corners_3d)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def roty(t):
|
def roty(t):
|
||||||
""" Rotation about the y-axis. """
|
""" Rotation about the y-axis. """
|
||||||
c = np.cos(t)
|
c = np.cos(t)
|
||||||
@ -66,7 +65,6 @@ def roty(t):
|
|||||||
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def project_to_image(pts_3d, P):
|
def project_to_image(pts_3d, P):
|
||||||
""" Project 3d points to image plane.
|
""" Project 3d points to image plane.
|
||||||
Usage: pts_2d = projectToImage(pts_3d, P)
|
Usage: pts_2d = projectToImage(pts_3d, P)
|
||||||
@ -87,7 +85,6 @@ def project_to_image(pts_3d, P):
|
|||||||
return pts_2d[:, 0:2]
|
return pts_2d[:, 0:2]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def project_8p_to_4p(pts_2d):
|
def project_8p_to_4p(pts_2d):
|
||||||
x0 = np.min(pts_2d[:, 0])
|
x0 = np.min(pts_2d[:, 0])
|
||||||
x1 = np.max(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
|
from setuptools import setup
|
||||||
|
|
||||||
# extract version from __init__.py
|
# This is needed for versioneer to be importable when building with PEP 517.
|
||||||
with open('monoloco/__init__.py', 'r') as f:
|
# See <https://github.com/warner/python-versioneer/issues/193> and links
|
||||||
VERSION_LINE = [l for l in f if l.startswith('__version__')][0]
|
# therein for more information.
|
||||||
VERSION = VERSION_LINE.split('=')[1].strip()[1:-1]
|
|
||||||
|
import os, sys
|
||||||
|
sys.path.append(os.path.dirname(__file__))
|
||||||
|
import versioneer
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='monoloco',
|
name='monoloco',
|
||||||
version=VERSION,
|
version=versioneer.get_version(),
|
||||||
|
cmdclass=versioneer.get_cmdclass(),
|
||||||
packages=[
|
packages=[
|
||||||
'monoloco',
|
'monoloco',
|
||||||
'monoloco.network',
|
'monoloco.network',
|
||||||
@ -29,15 +34,18 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'openpifpaf>=v0.12.1',
|
'openpifpaf>=v0.12.1',
|
||||||
'matplotlib',
|
'matplotlib',
|
||||||
'gdown',
|
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
|
'test': [
|
||||||
|
'pylint',
|
||||||
|
'pytest',
|
||||||
|
'gdown',
|
||||||
|
'scipy', # for social distancing gaussian blur
|
||||||
|
],
|
||||||
'eval': [
|
'eval': [
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'sklearn',
|
'sklearn',
|
||||||
'pandas',
|
'pandas',
|
||||||
'pylint',
|
|
||||||
'pytest',
|
|
||||||
],
|
],
|
||||||
'prep': [
|
'prep': [
|
||||||
'nuscenes-devkit==1.0.2',
|
'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 os
|
||||||
import sys
|
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
|
# Python does not consider the current directory to be a package
|
||||||
sys.path.insert(0, os.path.join('..', 'monoloco'))
|
sys.path.insert(0, os.path.join('..', 'monoloco'))
|
||||||
|
|
||||||
|
|
||||||
def test_iou():
|
def test_iou():
|
||||||
from stereoloco.utils import get_iou_matrix
|
|
||||||
boxes_pred = [[1, 100, 1, 200]]
|
boxes_pred = [[1, 100, 1, 200]]
|
||||||
boxes_gt = [[100., 120., 150., 160.],[12, 110, 130., 160.]]
|
boxes_gt = [[100., 120., 150., 160.],[12, 110, 130., 160.]]
|
||||||
iou_matrix = get_iou_matrix(boxes_pred, boxes_gt)
|
iou_matrix = get_iou_matrix(boxes_pred, boxes_gt)
|
||||||
@ -14,7 +16,6 @@ def test_iou():
|
|||||||
|
|
||||||
|
|
||||||
def test_pixel_to_camera():
|
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.]]
|
kk = [[718.3351, 0., 600.3891], [0., 718.3351, 181.5122], [0., 0., 1.]]
|
||||||
zz = 10
|
zz = 10
|
||||||
uv_vector = [1000., 400.]
|
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