killian31
commited on
Commit
·
8b09391
0
Parent(s):
initial commit
Browse files- .github/ISSUE_TEMPLATE/general-issue.yml +75 -0
- .github/workflows/github-actions-black-formatting.yml +20 -0
- .gitignore +12 -0
- CODE_OF_CONDUCT.md +128 -0
- LICENSE +201 -0
- README.md +115 -0
- app.py +151 -0
- images_to_video.py +52 -0
- main.py +302 -0
- poetry.lock +0 -0
- pyproject.toml +34 -0
- redirect.py +201 -0
- video_to_images.py +49 -0
.github/ISSUE_TEMPLATE/general-issue.yml
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: New Issue
|
2 |
+
description: Report an issue
|
3 |
+
title: "Issue Title"
|
4 |
+
body:
|
5 |
+
- type: markdown
|
6 |
+
attributes:
|
7 |
+
value: |
|
8 |
+
Thanks for taking the time to fill out this issue!
|
9 |
+
- type: textarea
|
10 |
+
id: what-happened
|
11 |
+
attributes:
|
12 |
+
label: What happened?
|
13 |
+
description: Describe your issue.
|
14 |
+
placeholder: Tell us what you see!
|
15 |
+
value: "Explain the issue here"
|
16 |
+
validations:
|
17 |
+
required: true
|
18 |
+
- type: textarea
|
19 |
+
id: expected
|
20 |
+
attributes:
|
21 |
+
label: Expected behavior
|
22 |
+
description: What did you expect to happen?
|
23 |
+
placeholder: What's the expected behavior?
|
24 |
+
value: "Describe the expected behavior here"
|
25 |
+
validations:
|
26 |
+
required: false
|
27 |
+
- type: textarea
|
28 |
+
id: logs
|
29 |
+
attributes:
|
30 |
+
label: Relevant log output
|
31 |
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
32 |
+
render: shell
|
33 |
+
- type: dropdown
|
34 |
+
id: os
|
35 |
+
attributes:
|
36 |
+
label: Operating System
|
37 |
+
description: What OS are you running?
|
38 |
+
options:
|
39 |
+
- Linux (Default)
|
40 |
+
- MacOS
|
41 |
+
- Windows
|
42 |
+
validations:
|
43 |
+
required: true
|
44 |
+
- type: textarea
|
45 |
+
id: os-version
|
46 |
+
attributes:
|
47 |
+
label: OS Version
|
48 |
+
description: What is the version of your os?
|
49 |
+
validations:
|
50 |
+
required: true
|
51 |
+
- type: dropdown
|
52 |
+
id: python-version
|
53 |
+
attributes:
|
54 |
+
label: Python version
|
55 |
+
multiple: true
|
56 |
+
options:
|
57 |
+
- 3.6
|
58 |
+
- 3.7
|
59 |
+
- 3.8 (default)
|
60 |
+
- 3.9
|
61 |
+
- 3.10
|
62 |
+
- 3.11
|
63 |
+
- type: textarea
|
64 |
+
id: other
|
65 |
+
attributes:
|
66 |
+
label: Any other information?
|
67 |
+
description: Please let us know any other information that can be useful for us to know
|
68 |
+
- type: checkboxes
|
69 |
+
id: terms
|
70 |
+
attributes:
|
71 |
+
label: Code of Conduct
|
72 |
+
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/killian31/VideoBackgroundRemoval/blob/main/CODE_OF_CONDUCT.md)
|
73 |
+
options:
|
74 |
+
- label: I agree to follow this project's Code of Conduct
|
75 |
+
required: true
|
.github/workflows/github-actions-black-formatting.yml
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: black
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: ["main"]
|
5 |
+
pull_request:
|
6 |
+
branches: ["main"]
|
7 |
+
|
8 |
+
jobs:
|
9 |
+
black:
|
10 |
+
runs-on: ubuntu-latest
|
11 |
+
steps:
|
12 |
+
- uses: actions/checkout@v3
|
13 |
+
- uses: actions/setup-python@v3
|
14 |
+
with:
|
15 |
+
python-version: "3.10"
|
16 |
+
- run: |
|
17 |
+
python -m pip install --upgrade pip
|
18 |
+
pip install black
|
19 |
+
- run: |
|
20 |
+
black --check --verbose .
|
.gitignore
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
frames/*
|
2 |
+
*.mov
|
3 |
+
*.MOV
|
4 |
+
*.mp4
|
5 |
+
*.MP4
|
6 |
+
*.jpg
|
7 |
+
*.JPG
|
8 |
+
__pycache__/*
|
9 |
+
*.txt
|
10 |
+
models/*
|
11 |
+
temp_images/*
|
12 |
+
.DS_Store
|
CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
+
identity and expression, level of experience, education, socio-economic status,
|
9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
10 |
+
and orientation.
|
11 |
+
|
12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13 |
+
diverse, inclusive, and healthy community.
|
14 |
+
|
15 |
+
## Our Standards
|
16 |
+
|
17 |
+
Examples of behavior that contributes to a positive environment for our
|
18 |
+
community include:
|
19 |
+
|
20 |
+
* Demonstrating empathy and kindness toward other people
|
21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22 |
+
* Giving and gracefully accepting constructive feedback
|
23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24 |
+
and learning from the experience
|
25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
26 |
+
overall community
|
27 |
+
|
28 |
+
Examples of unacceptable behavior include:
|
29 |
+
|
30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
31 |
+
advances of any kind
|
32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33 |
+
* Public or private harassment
|
34 |
+
* Publishing others' private information, such as a physical or email
|
35 |
+
address, without their explicit permission
|
36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
37 |
+
professional setting
|
38 |
+
|
39 |
+
## Enforcement Responsibilities
|
40 |
+
|
41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44 |
+
or harmful.
|
45 |
+
|
46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49 |
+
decisions when appropriate.
|
50 |
+
|
51 |
+
## Scope
|
52 |
+
|
53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
54 |
+
an individual is officially representing the community in public spaces.
|
55 |
+
Examples of representing our community include using an official e-mail address,
|
56 |
+
posting via an official social media account, or acting as an appointed
|
57 |
+
representative at an online or offline event.
|
58 |
+
|
59 |
+
## Enforcement
|
60 |
+
|
61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62 |
+
reported to the community leaders responsible for enforcement at
|
63 |
+
killian31.
|
64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
65 |
+
|
66 |
+
All community leaders are obligated to respect the privacy and security of the
|
67 |
+
reporter of any incident.
|
68 |
+
|
69 |
+
## Enforcement Guidelines
|
70 |
+
|
71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73 |
+
|
74 |
+
### 1. Correction
|
75 |
+
|
76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77 |
+
unprofessional or unwelcome in the community.
|
78 |
+
|
79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
80 |
+
clarity around the nature of the violation and an explanation of why the
|
81 |
+
behavior was inappropriate. A public apology may be requested.
|
82 |
+
|
83 |
+
### 2. Warning
|
84 |
+
|
85 |
+
**Community Impact**: A violation through a single incident or series
|
86 |
+
of actions.
|
87 |
+
|
88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
89 |
+
interaction with the people involved, including unsolicited interaction with
|
90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91 |
+
includes avoiding interactions in community spaces as well as external channels
|
92 |
+
like social media. Violating these terms may lead to a temporary or
|
93 |
+
permanent ban.
|
94 |
+
|
95 |
+
### 3. Temporary Ban
|
96 |
+
|
97 |
+
**Community Impact**: A serious violation of community standards, including
|
98 |
+
sustained inappropriate behavior.
|
99 |
+
|
100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101 |
+
communication with the community for a specified period of time. No public or
|
102 |
+
private interaction with the people involved, including unsolicited interaction
|
103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104 |
+
Violating these terms may lead to a permanent ban.
|
105 |
+
|
106 |
+
### 4. Permanent Ban
|
107 |
+
|
108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
111 |
+
|
112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
113 |
+
the community.
|
114 |
+
|
115 |
+
## Attribution
|
116 |
+
|
117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118 |
+
version 2.0, available at
|
119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
120 |
+
|
121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
123 |
+
|
124 |
+
[homepage]: https://www.contributor-covenant.org
|
125 |
+
|
126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
128 |
+
https://www.contributor-covenant.org/translations.
|
LICENSE
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Apache License
|
2 |
+
Version 2.0, January 2004
|
3 |
+
http://www.apache.org/licenses/
|
4 |
+
|
5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6 |
+
|
7 |
+
1. Definitions.
|
8 |
+
|
9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
11 |
+
|
12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13 |
+
the copyright owner that is granting the License.
|
14 |
+
|
15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
16 |
+
other entities that control, are controlled by, or are under common
|
17 |
+
control with that entity. For the purposes of this definition,
|
18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
19 |
+
direction or management of such entity, whether by contract or
|
20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22 |
+
|
23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24 |
+
exercising permissions granted by this License.
|
25 |
+
|
26 |
+
"Source" form shall mean the preferred form for making modifications,
|
27 |
+
including but not limited to software source code, documentation
|
28 |
+
source, and configuration files.
|
29 |
+
|
30 |
+
"Object" form shall mean any form resulting from mechanical
|
31 |
+
transformation or translation of a Source form, including but
|
32 |
+
not limited to compiled object code, generated documentation,
|
33 |
+
and conversions to other media types.
|
34 |
+
|
35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
36 |
+
Object form, made available under the License, as indicated by a
|
37 |
+
copyright notice that is included in or attached to the work
|
38 |
+
(an example is provided in the Appendix below).
|
39 |
+
|
40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41 |
+
form, that is based on (or derived from) the Work and for which the
|
42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
44 |
+
of this License, Derivative Works shall not include works that remain
|
45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46 |
+
the Work and Derivative Works thereof.
|
47 |
+
|
48 |
+
"Contribution" shall mean any work of authorship, including
|
49 |
+
the original version of the Work and any modifications or additions
|
50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
54 |
+
means any form of electronic, verbal, or written communication sent
|
55 |
+
to the Licensor or its representatives, including but not limited to
|
56 |
+
communication on electronic mailing lists, source code control systems,
|
57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
59 |
+
excluding communication that is conspicuously marked or otherwise
|
60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
61 |
+
|
62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
64 |
+
subsequently incorporated within the Work.
|
65 |
+
|
66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
71 |
+
Work and such Derivative Works in Source or Object form.
|
72 |
+
|
73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76 |
+
(except as stated in this section) patent license to make, have made,
|
77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78 |
+
where such license applies only to those patent claims licensable
|
79 |
+
by such Contributor that are necessarily infringed by their
|
80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
82 |
+
institute patent litigation against any entity (including a
|
83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84 |
+
or a Contribution incorporated within the Work constitutes direct
|
85 |
+
or contributory patent infringement, then any patent licenses
|
86 |
+
granted to You under this License for that Work shall terminate
|
87 |
+
as of the date such litigation is filed.
|
88 |
+
|
89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
90 |
+
Work or Derivative Works thereof in any medium, with or without
|
91 |
+
modifications, and in Source or Object form, provided that You
|
92 |
+
meet the following conditions:
|
93 |
+
|
94 |
+
(a) You must give any other recipients of the Work or
|
95 |
+
Derivative Works a copy of this License; and
|
96 |
+
|
97 |
+
(b) You must cause any modified files to carry prominent notices
|
98 |
+
stating that You changed the files; and
|
99 |
+
|
100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
101 |
+
that You distribute, all copyright, patent, trademark, and
|
102 |
+
attribution notices from the Source form of the Work,
|
103 |
+
excluding those notices that do not pertain to any part of
|
104 |
+
the Derivative Works; and
|
105 |
+
|
106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107 |
+
distribution, then any Derivative Works that You distribute must
|
108 |
+
include a readable copy of the attribution notices contained
|
109 |
+
within such NOTICE file, excluding those notices that do not
|
110 |
+
pertain to any part of the Derivative Works, in at least one
|
111 |
+
of the following places: within a NOTICE text file distributed
|
112 |
+
as part of the Derivative Works; within the Source form or
|
113 |
+
documentation, if provided along with the Derivative Works; or,
|
114 |
+
within a display generated by the Derivative Works, if and
|
115 |
+
wherever such third-party notices normally appear. The contents
|
116 |
+
of the NOTICE file are for informational purposes only and
|
117 |
+
do not modify the License. You may add Your own attribution
|
118 |
+
notices within Derivative Works that You distribute, alongside
|
119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
120 |
+
that such additional attribution notices cannot be construed
|
121 |
+
as modifying the License.
|
122 |
+
|
123 |
+
You may add Your own copyright statement to Your modifications and
|
124 |
+
may provide additional or different license terms and conditions
|
125 |
+
for use, reproduction, or distribution of Your modifications, or
|
126 |
+
for any such Derivative Works as a whole, provided Your use,
|
127 |
+
reproduction, and distribution of the Work otherwise complies with
|
128 |
+
the conditions stated in this License.
|
129 |
+
|
130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
132 |
+
by You to the Licensor shall be under the terms and conditions of
|
133 |
+
this License, without any additional terms or conditions.
|
134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135 |
+
the terms of any separate license agreement you may have executed
|
136 |
+
with Licensor regarding such Contributions.
|
137 |
+
|
138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
140 |
+
except as required for reasonable and customary use in describing the
|
141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
142 |
+
|
143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144 |
+
agreed to in writing, Licensor provides the Work (and each
|
145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147 |
+
implied, including, without limitation, any warranties or conditions
|
148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150 |
+
appropriateness of using or redistributing the Work and assume any
|
151 |
+
risks associated with Your exercise of permissions under this License.
|
152 |
+
|
153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
154 |
+
whether in tort (including negligence), contract, or otherwise,
|
155 |
+
unless required by applicable law (such as deliberate and grossly
|
156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157 |
+
liable to You for damages, including any direct, indirect, special,
|
158 |
+
incidental, or consequential damages of any character arising as a
|
159 |
+
result of this License or out of the use or inability to use the
|
160 |
+
Work (including but not limited to damages for loss of goodwill,
|
161 |
+
work stoppage, computer failure or malfunction, or any and all
|
162 |
+
other commercial damages or losses), even if such Contributor
|
163 |
+
has been advised of the possibility of such damages.
|
164 |
+
|
165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168 |
+
or other liability obligations and/or rights consistent with this
|
169 |
+
License. However, in accepting such obligations, You may act only
|
170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171 |
+
of any other Contributor, and only if You agree to indemnify,
|
172 |
+
defend, and hold each Contributor harmless for any liability
|
173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
174 |
+
of your accepting any such warranty or additional liability.
|
175 |
+
|
176 |
+
END OF TERMS AND CONDITIONS
|
177 |
+
|
178 |
+
APPENDIX: How to apply the Apache License to your work.
|
179 |
+
|
180 |
+
To apply the Apache License to your work, attach the following
|
181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182 |
+
replaced with your own identifying information. (Don't include
|
183 |
+
the brackets!) The text should be enclosed in the appropriate
|
184 |
+
comment syntax for the file format. We also recommend that a
|
185 |
+
file or class name and description of purpose be included on the
|
186 |
+
same "printed page" as the copyright notice for easier
|
187 |
+
identification within third-party archives.
|
188 |
+
|
189 |
+
Copyright 2023 killian31
|
190 |
+
|
191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192 |
+
you may not use this file except in compliance with the License.
|
193 |
+
You may obtain a copy of the License at
|
194 |
+
|
195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
196 |
+
|
197 |
+
Unless required by applicable law or agreed to in writing, software
|
198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200 |
+
See the License for the specific language governing permissions and
|
201 |
+
limitations under the License.
|
README.md
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI Powered Video Background Removal Tool
|
2 |
+
|
3 |
+
[](https://github.com/killian31/VideoBackgroundRemoval/stargazers)
|
4 |
+
[](https://github.com/killian31/VideoBackgroundRemoval/actions/workflows/github-actions-black-formatting.yml)
|
5 |
+
[](https://wakatime.com/badge/github/killian31/VideoBackgroundRemoval)
|
6 |
+
|
7 |
+
The Video Background Removal Tool is designed to enable users to effortlessly remove backgrounds from videos by selecting a subject in a single frame. This powerful tool is optimized to run on CPUs and boasts a user-friendly interface, making it ideal for a wide range of users, especially online content creators like YouTubers.
|
8 |
+
|
9 |
+
<p align="center">
|
10 |
+
<table>
|
11 |
+
<tr>
|
12 |
+
<td>
|
13 |
+
<img src="assets/example.gif" width="385" height="216" />
|
14 |
+
</td>
|
15 |
+
<td>
|
16 |
+
<img src="assets/output_example.gif" width="385" height="216" />
|
17 |
+
</td>
|
18 |
+
</tr>
|
19 |
+
</table>
|
20 |
+
</p>
|
21 |
+
|
22 |
+
## Contents
|
23 |
+
|
24 |
+
Table of contents:
|
25 |
+
|
26 |
+
- [Installation](#installation)
|
27 |
+
- [Usage](#usage)
|
28 |
+
- [Example](#example)
|
29 |
+
- [Contribution](#how-to-contribute)
|
30 |
+
|
31 |
+
## Installation
|
32 |
+
|
33 |
+
### With pyenv and poetry
|
34 |
+
|
35 |
+
```bash
|
36 |
+
git clone https://github.com/killian31/VideoBackgroundRemoval.git
|
37 |
+
cd VideoBackgroundRemoval
|
38 |
+
pyenv virtualenv 3.11.9 vbr
|
39 |
+
pyenv activate vbr
|
40 |
+
pip install poetry
|
41 |
+
poetry install
|
42 |
+
```
|
43 |
+
|
44 |
+
## Usage
|
45 |
+
|
46 |
+
### Using the Streamlit app (locally)
|
47 |
+
|
48 |
+
Run `streamlit run app.py` to launch the Streamlit app. Then, upload a video, draw a
|
49 |
+
bounding box around what you want to remove the background from, using the sliders,
|
50 |
+
and click on Segment Video.
|
51 |
+
|
52 |
+
### Command line
|
53 |
+
|
54 |
+
```bash
|
55 |
+
usage: main.py [-h] [--video_filename VIDEO_FILENAME] [--dir_frames DIR_FRAMES] [--image_start IMAGE_START] [--image_end IMAGE_END] [--bbox_file BBOX_FILE] [--skip_vid2im]
|
56 |
+
[--mobile_sam_weights MOBILE_SAM_WEIGHTS] [--tracker_name {yolov7,yoloS}] [--output_dir OUTPUT_DIR] [--output_video OUTPUT_VIDEO] [--auto_detect]
|
57 |
+
[--background_color BACKGROUND_COLOR]
|
58 |
+
|
59 |
+
options:
|
60 |
+
-h, --help show this help message and exit
|
61 |
+
--video_filename VIDEO_FILENAME
|
62 |
+
path to the video
|
63 |
+
--dir_frames DIR_FRAMES
|
64 |
+
path to the directory in which all input frames will be stored
|
65 |
+
--image_start IMAGE_START
|
66 |
+
first image to be stored
|
67 |
+
--image_end IMAGE_END
|
68 |
+
last image to be stored, last one if 0
|
69 |
+
--bbox_file BBOX_FILE
|
70 |
+
path to the bounding box text file
|
71 |
+
--skip_vid2im whether to write the video frames as images
|
72 |
+
--mobile_sam_weights MOBILE_SAM_WEIGHTS
|
73 |
+
path to MobileSAM weights
|
74 |
+
--tracker_name {yolov7,yoloS}
|
75 |
+
tracker name
|
76 |
+
--output_dir OUTPUT_DIR
|
77 |
+
directory to store the output frames
|
78 |
+
--output_video OUTPUT_VIDEO
|
79 |
+
path to store the output video
|
80 |
+
--auto_detect whether to use a bounding box to force the model to segment the object
|
81 |
+
--background_color BACKGROUND_COLOR
|
82 |
+
background color for the output (hex)
|
83 |
+
```
|
84 |
+
|
85 |
+
## Example
|
86 |
+
|
87 |
+
The following command line is a working example from a video stored in the repo:
|
88 |
+
|
89 |
+
```bash
|
90 |
+
python3 main.py --video_filename assets/example.mp4 --dir_frames ./frames --bbox_file bbox.txt --mobile_sam_weights models/mobile_sam.pt --output_dir output_frames --output_video output.mp4
|
91 |
+
```
|
92 |
+
|
93 |
+
## How to Contribute
|
94 |
+
|
95 |
+
We welcome contributions from the community! To ensure a consistent code style, we ask contributors to follow these guidelines:
|
96 |
+
|
97 |
+
### Code Format
|
98 |
+
|
99 |
+
Please format your code using the `black` code formatter.
|
100 |
+
|
101 |
+
#### Installation
|
102 |
+
|
103 |
+
```bash
|
104 |
+
pip install black
|
105 |
+
```
|
106 |
+
|
107 |
+
#### Usage
|
108 |
+
|
109 |
+
To format your code:
|
110 |
+
|
111 |
+
```bash
|
112 |
+
black .
|
113 |
+
```
|
114 |
+
|
115 |
+
This setup will help maintain a consistent coding style throughout the project.
|
app.py
ADDED
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import warnings
|
3 |
+
|
4 |
+
import cv2
|
5 |
+
import streamlit as st
|
6 |
+
from PIL import Image, ImageDraw
|
7 |
+
|
8 |
+
import redirect as rd
|
9 |
+
from main import segment_video
|
10 |
+
|
11 |
+
warnings.filterwarnings("ignore")
|
12 |
+
|
13 |
+
|
14 |
+
def load_image(image_path):
|
15 |
+
return Image.open(image_path)
|
16 |
+
|
17 |
+
|
18 |
+
def extract_first_frame(video_path, output_image_path):
|
19 |
+
"""
|
20 |
+
Extract the first frame from a video file and save it to disk.
|
21 |
+
|
22 |
+
Parameters:
|
23 |
+
video_path (str): Path to the video file.
|
24 |
+
output_image_path (str): Path to save the extracted frame.
|
25 |
+
|
26 |
+
Returns:
|
27 |
+
str: Path to the saved frame.
|
28 |
+
"""
|
29 |
+
cap = cv2.VideoCapture(video_path)
|
30 |
+
if not cap.isOpened():
|
31 |
+
raise ValueError(f"Error: Unable to open video file: {video_path}")
|
32 |
+
ret, frame = cap.read()
|
33 |
+
cap.release()
|
34 |
+
if not ret:
|
35 |
+
raise ValueError("Error: Unable to read the first frame from the video.")
|
36 |
+
cv2.imwrite(output_image_path, frame)
|
37 |
+
|
38 |
+
return output_image_path
|
39 |
+
|
40 |
+
|
41 |
+
st.title("Video Background Removal")
|
42 |
+
|
43 |
+
st.write(
|
44 |
+
"This app uses the Mobile-SAM model to remove the background from a video. "
|
45 |
+
"The model is based on the paper [Faster Segment Anything: Towards Lightweight SAM for Mobile Applications](https://arxiv.org/abs/2306.14289)."
|
46 |
+
)
|
47 |
+
st.write(
|
48 |
+
"How to use: Upload a video and click 'Segment Video'. The app will then process the video and remove the background. "
|
49 |
+
"You can also use a bounding box to specify the area to segment. "
|
50 |
+
"The app will then output the segmented video, that you can download. "
|
51 |
+
"Do not hesitate to hit the 'Stop/Reset' button if you encounter any issues (it usually solves them all) or want to start over."
|
52 |
+
)
|
53 |
+
|
54 |
+
|
55 |
+
video_file = st.file_uploader("Upload a video", type=["mp4", "avi", "mov"])
|
56 |
+
|
57 |
+
if video_file is not None:
|
58 |
+
st.video(video_file)
|
59 |
+
with open("temp_video.mp4", "wb") as f:
|
60 |
+
f.write(video_file.getbuffer())
|
61 |
+
|
62 |
+
if not os.path.exists("./temp_images"):
|
63 |
+
os.makedirs("./temp_images")
|
64 |
+
frame_path = extract_first_frame("temp_video.mp4", "temp_frame.jpg")
|
65 |
+
|
66 |
+
use_bbox = st.checkbox("Use bounding box", value=False)
|
67 |
+
background_color = st.color_picker("Background keying color", "#009000")
|
68 |
+
|
69 |
+
initial_frame = load_image(frame_path)
|
70 |
+
original_width, original_height = initial_frame.width, initial_frame.height
|
71 |
+
if use_bbox:
|
72 |
+
col1, col2 = st.columns(2)
|
73 |
+
|
74 |
+
with col1:
|
75 |
+
xmin = st.slider("xmin", 0, original_width, original_width // 4)
|
76 |
+
ymin = st.slider("ymin", 0, original_height, original_height // 4)
|
77 |
+
with col2:
|
78 |
+
xmax = st.slider("xmax", 0, original_width, original_width // 2)
|
79 |
+
ymax = st.slider("ymax", 0, original_height, original_height // 2)
|
80 |
+
|
81 |
+
draw = ImageDraw.Draw(initial_frame)
|
82 |
+
draw.rectangle([xmin, ymin, xmax, ymax], outline="red", width=3)
|
83 |
+
st.image(initial_frame, caption="Bounding Box Preview", use_column_width=True)
|
84 |
+
if st.button("Save Bounding Box"):
|
85 |
+
with open("temp_bbox.txt", "w") as bbox_file:
|
86 |
+
bbox_file.write(f"{xmin} {ymin} {xmax} {ymax}")
|
87 |
+
st.write(f"Bounding box saved to {os.path.abspath('temp_bbox.txt')}")
|
88 |
+
|
89 |
+
col1, col2 = st.columns(2)
|
90 |
+
with col2:
|
91 |
+
if st.button(
|
92 |
+
"Stop/Reset",
|
93 |
+
key="stop",
|
94 |
+
help="Stop the process and reset the app",
|
95 |
+
type="primary",
|
96 |
+
):
|
97 |
+
st.write("Stopping...")
|
98 |
+
os.system("rm -r ./temp_images")
|
99 |
+
os.system("rm ./temp_bbox.txt")
|
100 |
+
os.system("rm -r ./temp_processed_images")
|
101 |
+
os.system("rm ./temp_video.mp4")
|
102 |
+
os.system("rm ./temp_frame.jpg")
|
103 |
+
st.write("Process interrupted")
|
104 |
+
|
105 |
+
with col1:
|
106 |
+
if st.button(
|
107 |
+
"Segment Video", key="segment", help="Segment the video", type="secondary"
|
108 |
+
):
|
109 |
+
if use_bbox:
|
110 |
+
if not os.path.exists("./temp_bbox.txt"):
|
111 |
+
with open("temp_bbox.txt", "w") as bbox_file:
|
112 |
+
bbox_file.write(f"{xmin} {ymin} {xmax} {ymax}")
|
113 |
+
else:
|
114 |
+
with open("temp_bbox.txt", "w") as bbox_file:
|
115 |
+
bbox_file.write(f"0 0 {original_width} {original_height}")
|
116 |
+
|
117 |
+
st.write("Segmenting video...")
|
118 |
+
so = st.empty()
|
119 |
+
with rd.stdouterr(to=st.sidebar):
|
120 |
+
segment_video(
|
121 |
+
video_filename="temp_video.mp4",
|
122 |
+
dir_frames="temp_images",
|
123 |
+
image_start=0,
|
124 |
+
image_end=0,
|
125 |
+
bbox_file="temp_bbox.txt",
|
126 |
+
skip_vid2im=False,
|
127 |
+
mobile_sam_weights="./models/mobile_sam.pt",
|
128 |
+
auto_detect=not use_bbox,
|
129 |
+
background_color=background_color,
|
130 |
+
output_video="video_segmented.mp4",
|
131 |
+
output_dir="temp_processed_images",
|
132 |
+
pbar=False,
|
133 |
+
reverse_mask=not use_bbox,
|
134 |
+
)
|
135 |
+
|
136 |
+
os.system("rm -rf ./temp_images")
|
137 |
+
os.system("rm -rf ./temp_bbox.txt")
|
138 |
+
os.system("rm -rf ./temp_processed_images")
|
139 |
+
os.system("rm -rf ./temp_video.mp4")
|
140 |
+
|
141 |
+
st.video("./video_segmented.mp4")
|
142 |
+
st.write(f"Video saved to {os.path.abspath('video_segmented.mp4')}")
|
143 |
+
|
144 |
+
vid_file = open("video_segmented.mp4", "rb")
|
145 |
+
vid_bytes = vid_file.read()
|
146 |
+
st.download_button(
|
147 |
+
label="Download Segmented Video",
|
148 |
+
data=vid_bytes,
|
149 |
+
file_name="video_segmented.mp4",
|
150 |
+
)
|
151 |
+
vid_file.close()
|
images_to_video.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
import cv2
|
4 |
+
from tqdm import tqdm
|
5 |
+
|
6 |
+
|
7 |
+
class VideoCreator:
|
8 |
+
def __init__(self, imgs_dir, vid_name, pbar=True):
|
9 |
+
"""
|
10 |
+
:param str imgs_dir: The directory where the image files are stored.
|
11 |
+
:param str vid_name: The name of the video's filename.
|
12 |
+
:param bool pbar: Whether to display a progress bar.
|
13 |
+
"""
|
14 |
+
|
15 |
+
self.imgs_dir = imgs_dir
|
16 |
+
self.img_array = []
|
17 |
+
self.video_filename = vid_name
|
18 |
+
self.pbar = pbar
|
19 |
+
|
20 |
+
def preprocess_images(self):
|
21 |
+
filenames = sorted(os.listdir(self.imgs_dir))
|
22 |
+
print("Adding images...")
|
23 |
+
if self.pbar:
|
24 |
+
pb = tqdm(filenames)
|
25 |
+
else:
|
26 |
+
pb = filenames
|
27 |
+
|
28 |
+
height, width, _ = cv2.imread(self.imgs_dir + "/" + filenames[0]).shape
|
29 |
+
size = (width, height)
|
30 |
+
for filename in pb:
|
31 |
+
complete_filename = self.imgs_dir + "/" + filename
|
32 |
+
img = cv2.imread(complete_filename)
|
33 |
+
# convert to BGR
|
34 |
+
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
35 |
+
self.img_array.append(img)
|
36 |
+
|
37 |
+
return size
|
38 |
+
|
39 |
+
def create_video(self, fps=20):
|
40 |
+
size = self.preprocess_images()
|
41 |
+
out = cv2.VideoWriter(
|
42 |
+
self.video_filename, cv2.VideoWriter_fourcc(*"MJPG"), fps, size
|
43 |
+
)
|
44 |
+
print("Recording video...")
|
45 |
+
if self.pbar:
|
46 |
+
pb = tqdm(range(len(self.img_array)))
|
47 |
+
else:
|
48 |
+
pb = range(len(self.img_array))
|
49 |
+
for i in pb:
|
50 |
+
out.write(self.img_array[i])
|
51 |
+
out.release()
|
52 |
+
print("Done.")
|
main.py
ADDED
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
|
5 |
+
import cv2
|
6 |
+
import numpy as np
|
7 |
+
import requests
|
8 |
+
import torch
|
9 |
+
import wget
|
10 |
+
import yolov7
|
11 |
+
from mobile_sam import SamPredictor, sam_model_registry
|
12 |
+
from PIL import Image
|
13 |
+
from tqdm import tqdm
|
14 |
+
from transformers import YolosForObjectDetection, YolosImageProcessor
|
15 |
+
|
16 |
+
from images_to_video import VideoCreator
|
17 |
+
from video_to_images import ImageCreator
|
18 |
+
|
19 |
+
|
20 |
+
def download_mobile_sam_weight(path):
|
21 |
+
if not os.path.exists(path):
|
22 |
+
sam_weights = "https://raw.githubusercontent.com/ChaoningZhang/MobileSAM/master/weights/mobile_sam.pt"
|
23 |
+
for i in range(2, len(path.split("/"))):
|
24 |
+
temp = path.split("/")[:i]
|
25 |
+
cur_path = "/".join(temp)
|
26 |
+
if not os.path.isdir(cur_path):
|
27 |
+
os.mkdir(cur_path)
|
28 |
+
model_name = path.split("/")[-1]
|
29 |
+
if model_name in sam_weights:
|
30 |
+
wget.download(sam_weights, path)
|
31 |
+
else:
|
32 |
+
raise NameError(
|
33 |
+
"There is no pretrained weight to download for %s, you need to provide a path to segformer weights."
|
34 |
+
% model_name
|
35 |
+
)
|
36 |
+
|
37 |
+
|
38 |
+
def get_closest_bbox(bbox_list, bbox_target):
|
39 |
+
"""
|
40 |
+
Given a list of bounding boxes, find the one that is closest to the target bounding box.
|
41 |
+
Args:
|
42 |
+
bbox_list: list of bounding boxes
|
43 |
+
bbox_target: target bounding box
|
44 |
+
Returns:
|
45 |
+
closest bounding box
|
46 |
+
|
47 |
+
"""
|
48 |
+
min_dist = 100000000
|
49 |
+
min_idx = 0
|
50 |
+
for idx, bbox in enumerate(bbox_list):
|
51 |
+
dist = np.linalg.norm(bbox - bbox_target)
|
52 |
+
if dist < min_dist:
|
53 |
+
min_dist = dist
|
54 |
+
min_idx = idx
|
55 |
+
return bbox_list[min_idx]
|
56 |
+
|
57 |
+
|
58 |
+
def get_bboxes(image_file, image, model, image_processor, threshold=0.9):
|
59 |
+
if image_processor is None:
|
60 |
+
results = model(image_file)
|
61 |
+
predictions = results.pred[0]
|
62 |
+
boxes = predictions[:, :4].detach().numpy()
|
63 |
+
return boxes
|
64 |
+
else:
|
65 |
+
inputs = image_processor(images=image, return_tensors="pt")
|
66 |
+
outputs = model(**inputs)
|
67 |
+
|
68 |
+
target_sizes = torch.tensor([image.size[::-1]])
|
69 |
+
results = image_processor.post_process_object_detection(
|
70 |
+
outputs, threshold=threshold, target_sizes=target_sizes
|
71 |
+
)[0]
|
72 |
+
|
73 |
+
return results["boxes"].detach().numpy()
|
74 |
+
|
75 |
+
|
76 |
+
def segment_video(
|
77 |
+
video_filename,
|
78 |
+
dir_frames,
|
79 |
+
image_start,
|
80 |
+
image_end,
|
81 |
+
bbox_file,
|
82 |
+
skip_vid2im,
|
83 |
+
mobile_sam_weights,
|
84 |
+
auto_detect=False,
|
85 |
+
tracker_name="yolov7",
|
86 |
+
background_color="#009000",
|
87 |
+
output_dir="output_frames",
|
88 |
+
output_video="output.mp4",
|
89 |
+
pbar=False,
|
90 |
+
reverse_mask=False,
|
91 |
+
):
|
92 |
+
if not skip_vid2im:
|
93 |
+
vid_to_im = ImageCreator(
|
94 |
+
video_filename,
|
95 |
+
dir_frames,
|
96 |
+
image_start=image_start,
|
97 |
+
image_end=image_end,
|
98 |
+
pbar=pbar,
|
99 |
+
)
|
100 |
+
vid_to_im.get_images()
|
101 |
+
# Get fps of video
|
102 |
+
vid = cv2.VideoCapture(video_filename)
|
103 |
+
fps = vid.get(cv2.CAP_PROP_FPS)
|
104 |
+
vid.release()
|
105 |
+
background_color = background_color.lstrip("#")
|
106 |
+
background_color = (
|
107 |
+
np.array([int(background_color[i : i + 2], 16) for i in (0, 2, 4)]) / 255.0
|
108 |
+
)
|
109 |
+
|
110 |
+
with open(bbox_file, "r") as f:
|
111 |
+
bbox_orig = [int(coord) for coord in f.read().split(" ")]
|
112 |
+
download_mobile_sam_weight(mobile_sam_weights)
|
113 |
+
if image_end == 0:
|
114 |
+
frames = sorted(os.listdir(dir_frames))[image_start:]
|
115 |
+
else:
|
116 |
+
frames = sorted(os.listdir(dir_frames))[image_start:image_end]
|
117 |
+
|
118 |
+
model_type = "vit_t"
|
119 |
+
|
120 |
+
if torch.backends.mps.is_available():
|
121 |
+
device = "mps"
|
122 |
+
elif torch.cuda.is_available():
|
123 |
+
|
124 |
+
device = "cuda"
|
125 |
+
else:
|
126 |
+
device = "cpu"
|
127 |
+
sam = sam_model_registry[model_type](checkpoint=mobile_sam_weights)
|
128 |
+
sam.to(device=device)
|
129 |
+
sam.eval()
|
130 |
+
|
131 |
+
predictor = SamPredictor(sam)
|
132 |
+
|
133 |
+
if not auto_detect:
|
134 |
+
if tracker_name == "yolov7":
|
135 |
+
model = yolov7.load("kadirnar/yolov7-tiny-v0.1", hf_model=True)
|
136 |
+
model.conf = 0.25 # NMS confidence threshold
|
137 |
+
model.iou = 0.45 # NMS IoU threshold
|
138 |
+
model.classes = None
|
139 |
+
image_processor = None
|
140 |
+
else:
|
141 |
+
model = YolosForObjectDetection.from_pretrained("hustvl/yolos-tiny")
|
142 |
+
image_processor = YolosImageProcessor.from_pretrained("hustvl/yolos-tiny")
|
143 |
+
|
144 |
+
output_frames = []
|
145 |
+
|
146 |
+
if pbar:
|
147 |
+
pb = tqdm(frames)
|
148 |
+
else:
|
149 |
+
pb = frames
|
150 |
+
|
151 |
+
processed_frames = 0
|
152 |
+
init_time = time.time()
|
153 |
+
for frame in pb:
|
154 |
+
processed_frames += 1
|
155 |
+
image_file = dir_frames + "/" + frame
|
156 |
+
image_pil = Image.open(image_file)
|
157 |
+
image_np = np.array(image_pil)
|
158 |
+
if not auto_detect:
|
159 |
+
bboxes = get_bboxes(image_file, image_pil, model, image_processor)
|
160 |
+
closest_bbox = get_closest_bbox(bboxes, bbox_orig)
|
161 |
+
input_box = np.array(closest_bbox)
|
162 |
+
else:
|
163 |
+
input_box = np.array([0, 0, image_np.shape[1], image_np.shape[0]])
|
164 |
+
predictor.set_image(image_np)
|
165 |
+
masks, _, _ = predictor.predict(
|
166 |
+
point_coords=None,
|
167 |
+
point_labels=None,
|
168 |
+
box=input_box[None, :],
|
169 |
+
multimask_output=True,
|
170 |
+
)
|
171 |
+
if reverse_mask:
|
172 |
+
mask = masks[0]
|
173 |
+
h, w = mask.shape[-2:]
|
174 |
+
mask_image = (
|
175 |
+
(mask).reshape(h, w, 1) * background_color.reshape(1, 1, -1)
|
176 |
+
) * 255
|
177 |
+
masked_image = image_np * (1 - mask).reshape(h, w, 1)
|
178 |
+
masked_image = masked_image + mask_image
|
179 |
+
output_frames.append(masked_image)
|
180 |
+
else:
|
181 |
+
mask = masks[0]
|
182 |
+
h, w = mask.shape[-2:]
|
183 |
+
mask_image = (
|
184 |
+
(1 - mask).reshape(h, w, 1) * background_color.reshape(1, 1, -1)
|
185 |
+
) * 255
|
186 |
+
masked_image = image_np * mask.reshape(h, w, 1)
|
187 |
+
masked_image = masked_image + mask_image
|
188 |
+
output_frames.append(masked_image)
|
189 |
+
|
190 |
+
if not pbar and processed_frames % 10 == 0:
|
191 |
+
remaining_time = (
|
192 |
+
(time.time() - init_time)
|
193 |
+
/ processed_frames
|
194 |
+
* (len(frames) - processed_frames)
|
195 |
+
)
|
196 |
+
remaining_time = int(remaining_time)
|
197 |
+
remaining_time_str = f"{remaining_time//60}m {remaining_time%60}s"
|
198 |
+
print(
|
199 |
+
f"Processed frame {processed_frames}/{len(frames)} - Remaining time: {remaining_time_str}"
|
200 |
+
)
|
201 |
+
if not os.path.exists(output_dir):
|
202 |
+
os.mkdir(output_dir)
|
203 |
+
|
204 |
+
zfill_max = len(str(len(output_frames)))
|
205 |
+
for idx, frame in enumerate(output_frames):
|
206 |
+
cv2.imwrite(
|
207 |
+
f"{output_dir}/frame_{str(idx).zfill(zfill_max)}.png",
|
208 |
+
frame,
|
209 |
+
)
|
210 |
+
vid_creator = VideoCreator(output_dir, output_video, pbar=pbar)
|
211 |
+
vid_creator.create_video(fps=int(fps))
|
212 |
+
|
213 |
+
|
214 |
+
if __name__ == "__main__":
|
215 |
+
parser = argparse.ArgumentParser()
|
216 |
+
parser.add_argument(
|
217 |
+
"--video_filename",
|
218 |
+
default="assets/example.mp4",
|
219 |
+
type=str,
|
220 |
+
help="path to the video",
|
221 |
+
)
|
222 |
+
parser.add_argument(
|
223 |
+
"--dir_frames",
|
224 |
+
type=str,
|
225 |
+
default="frames",
|
226 |
+
help="path to the directory in which all input frames will be stored",
|
227 |
+
)
|
228 |
+
parser.add_argument(
|
229 |
+
"--image_start", type=int, default=0, help="first image to be stored"
|
230 |
+
)
|
231 |
+
parser.add_argument(
|
232 |
+
"--image_end",
|
233 |
+
type=int,
|
234 |
+
default=0,
|
235 |
+
help="last image to be stored, last one if 0",
|
236 |
+
)
|
237 |
+
parser.add_argument(
|
238 |
+
"--bbox_file",
|
239 |
+
type=str,
|
240 |
+
default="bbox.txt",
|
241 |
+
help="path to the bounding box text file",
|
242 |
+
)
|
243 |
+
parser.add_argument(
|
244 |
+
"--skip_vid2im",
|
245 |
+
action="store_true",
|
246 |
+
help="whether to write the video frames as images",
|
247 |
+
)
|
248 |
+
parser.add_argument(
|
249 |
+
"--mobile_sam_weights",
|
250 |
+
type=str,
|
251 |
+
default="./models/mobile_sam.pt",
|
252 |
+
help="path to MobileSAM weights",
|
253 |
+
)
|
254 |
+
|
255 |
+
parser.add_argument(
|
256 |
+
"--tracker_name",
|
257 |
+
type=str,
|
258 |
+
default="yolov7",
|
259 |
+
help="tracker name",
|
260 |
+
choices=["yolov7", "yoloS"],
|
261 |
+
)
|
262 |
+
|
263 |
+
parser.add_argument(
|
264 |
+
"--output_dir",
|
265 |
+
type=str,
|
266 |
+
default="output_frames",
|
267 |
+
help="directory to store the output frames",
|
268 |
+
)
|
269 |
+
|
270 |
+
parser.add_argument(
|
271 |
+
"--output_video",
|
272 |
+
type=str,
|
273 |
+
default="output.mp4",
|
274 |
+
help="path to store the output video",
|
275 |
+
)
|
276 |
+
parser.add_argument(
|
277 |
+
"--auto_detect",
|
278 |
+
action="store_true",
|
279 |
+
help="whether to use a bounding box to force the model to segment the object",
|
280 |
+
)
|
281 |
+
parser.add_argument(
|
282 |
+
"--background_color",
|
283 |
+
type=str,
|
284 |
+
default="#009000",
|
285 |
+
help="background color for the output (hex)",
|
286 |
+
)
|
287 |
+
args = parser.parse_args()
|
288 |
+
|
289 |
+
segment_video(
|
290 |
+
args.video_filename,
|
291 |
+
args.dir_frames,
|
292 |
+
args.image_start,
|
293 |
+
args.image_end,
|
294 |
+
args.bbox_file,
|
295 |
+
args.skip_vid2im,
|
296 |
+
args.mobile_sam_weights,
|
297 |
+
args.auto_detect,
|
298 |
+
args.output_dir,
|
299 |
+
args.output_video,
|
300 |
+
args.tracker_name,
|
301 |
+
args.background_color,
|
302 |
+
)
|
poetry.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
pyproject.toml
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "vbr"
|
3 |
+
version = "1.0.0"
|
4 |
+
description = "Automatic background removal from an input video and a single user subject selection."
|
5 |
+
authors = [
|
6 |
+
{name = "killian31",email = "[email protected]"}
|
7 |
+
]
|
8 |
+
license = {text = "Apache 2.0"}
|
9 |
+
readme = "README.md"
|
10 |
+
requires-python = ">=3.11"
|
11 |
+
dependencies = [
|
12 |
+
"numpy (==1.26.4)",
|
13 |
+
"opencv-python (==4.9.0.80)",
|
14 |
+
"opencv-python-headless (>=4.9.0.80,<4.10.0.0)",
|
15 |
+
"pillow (>=10.2.0,<10.3.0)",
|
16 |
+
"requests (>=2.31.0,<2.32.0)",
|
17 |
+
"streamlit (>=1.31.0,<1.32.0)",
|
18 |
+
"timm (>=0.9.12,<0.10.0)",
|
19 |
+
"torch (==2.2.2)",
|
20 |
+
"tqdm (>=4.66.1,<4.67.0)",
|
21 |
+
"transformers (>=4.37.2,<4.38.0)",
|
22 |
+
"wget (>=3.2,<4.0)",
|
23 |
+
"yolov7detect (>=1.0.1,<1.1.0)",
|
24 |
+
"mobile-sam @ git+https://github.com/ChaoningZhang/MobileSAM.git",
|
25 |
+
"huggingface-hub (==0.24.7)"
|
26 |
+
]
|
27 |
+
|
28 |
+
|
29 |
+
[build-system]
|
30 |
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
31 |
+
build-backend = "poetry.core.masonry.api"
|
32 |
+
|
33 |
+
[tool.poetry]
|
34 |
+
package-mode = false
|
redirect.py
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import contextlib
|
2 |
+
import io
|
3 |
+
import re
|
4 |
+
import sys
|
5 |
+
import threading
|
6 |
+
|
7 |
+
import streamlit as st
|
8 |
+
|
9 |
+
|
10 |
+
class _Redirect:
|
11 |
+
class IOStuff(io.StringIO):
|
12 |
+
def __init__(
|
13 |
+
self, trigger, max_buffer, buffer_separator, regex, dup, need_dup, on_thread
|
14 |
+
):
|
15 |
+
super().__init__()
|
16 |
+
self._trigger = trigger
|
17 |
+
self._max_buffer = max_buffer
|
18 |
+
self._buffer_separator = buffer_separator
|
19 |
+
self._regex = regex and re.compile(regex)
|
20 |
+
self._dup = dup
|
21 |
+
self._need_dup = need_dup
|
22 |
+
self._on_thread = on_thread
|
23 |
+
|
24 |
+
def write(self, __s: str) -> int:
|
25 |
+
res = None
|
26 |
+
if self._on_thread == threading.get_ident():
|
27 |
+
if self._max_buffer:
|
28 |
+
concatenated_len = super().tell() + len(__s)
|
29 |
+
if concatenated_len > self._max_buffer:
|
30 |
+
rest = self.get_filtered_output()[
|
31 |
+
concatenated_len - self._max_buffer :
|
32 |
+
]
|
33 |
+
if self._buffer_separator is not None:
|
34 |
+
rest = rest.split(self._buffer_separator, 1)[-1]
|
35 |
+
super().seek(0)
|
36 |
+
super().write(rest)
|
37 |
+
super().truncate(super().tell() + len(__s))
|
38 |
+
res = super().write(__s)
|
39 |
+
self._trigger(self.get_filtered_output())
|
40 |
+
if self._on_thread != threading.get_ident() or self._need_dup:
|
41 |
+
self._dup.write(__s)
|
42 |
+
return res
|
43 |
+
|
44 |
+
def get_filtered_output(self):
|
45 |
+
if self._regex is None or self._buffer_separator is None:
|
46 |
+
return self.getvalue()
|
47 |
+
|
48 |
+
return self._buffer_separator.join(
|
49 |
+
filter(
|
50 |
+
self._regex.search, self.getvalue().split(self._buffer_separator)
|
51 |
+
)
|
52 |
+
)
|
53 |
+
|
54 |
+
def print_at_end(self):
|
55 |
+
self._trigger(self.get_filtered_output())
|
56 |
+
|
57 |
+
def __init__(
|
58 |
+
self,
|
59 |
+
stdout=None,
|
60 |
+
stderr=False,
|
61 |
+
format=None,
|
62 |
+
to=None,
|
63 |
+
max_buffer=None,
|
64 |
+
buffer_separator="\n",
|
65 |
+
regex=None,
|
66 |
+
duplicate_out=False,
|
67 |
+
):
|
68 |
+
self.io_args = {
|
69 |
+
"trigger": self._write,
|
70 |
+
"max_buffer": max_buffer,
|
71 |
+
"buffer_separator": buffer_separator,
|
72 |
+
"regex": regex,
|
73 |
+
"on_thread": threading.get_ident(),
|
74 |
+
}
|
75 |
+
self.redirections = []
|
76 |
+
self.st = None
|
77 |
+
self.stderr = stderr is True
|
78 |
+
self.stdout = stdout is True or (stdout is None and not self.stderr)
|
79 |
+
self.format = format or "code"
|
80 |
+
self.to = to
|
81 |
+
self.fun = None
|
82 |
+
self.duplicate_out = duplicate_out or None
|
83 |
+
self.active_nested = None
|
84 |
+
|
85 |
+
if not self.stdout and not self.stderr:
|
86 |
+
raise ValueError("one of stdout or stderr must be True")
|
87 |
+
|
88 |
+
if self.format not in ["text", "markdown", "latex", "code", "write"]:
|
89 |
+
raise ValueError(
|
90 |
+
f"format need oneof the following: {', '.join(['text', 'markdown', 'latex', 'code', 'write'])}"
|
91 |
+
)
|
92 |
+
|
93 |
+
if self.to and (not hasattr(self.to, "text") or not hasattr(self.to, "empty")):
|
94 |
+
raise ValueError(f"'to' is not a streamlit container object")
|
95 |
+
|
96 |
+
def __enter__(self):
|
97 |
+
if self.st is not None:
|
98 |
+
if self.to is None:
|
99 |
+
if self.active_nested is None:
|
100 |
+
self.active_nested = self(
|
101 |
+
format=self.format,
|
102 |
+
max_buffer=self.io_args["max_buffer"],
|
103 |
+
buffer_separator=self.io_args["buffer_separator"],
|
104 |
+
regex=self.io_args["regex"],
|
105 |
+
duplicate_out=self.duplicate_out,
|
106 |
+
)
|
107 |
+
return self.active_nested.__enter__()
|
108 |
+
else:
|
109 |
+
raise Exception("Already entered")
|
110 |
+
to = self.to or st
|
111 |
+
|
112 |
+
to.text("Logs:")
|
113 |
+
self.st = to.empty()
|
114 |
+
self.fun = getattr(self.st, self.format)
|
115 |
+
|
116 |
+
io_obj = None
|
117 |
+
|
118 |
+
def redirect(to_duplicate, context_redirect):
|
119 |
+
nonlocal io_obj
|
120 |
+
io_obj = _Redirect.IOStuff(
|
121 |
+
need_dup=self.duplicate_out and True, dup=to_duplicate, **self.io_args
|
122 |
+
)
|
123 |
+
redirection = context_redirect(io_obj)
|
124 |
+
self.redirections.append((redirection, io_obj))
|
125 |
+
redirection.__enter__()
|
126 |
+
|
127 |
+
if self.stderr:
|
128 |
+
redirect(sys.stderr, contextlib.redirect_stderr)
|
129 |
+
if self.stdout:
|
130 |
+
redirect(sys.stdout, contextlib.redirect_stdout)
|
131 |
+
|
132 |
+
return io_obj
|
133 |
+
|
134 |
+
def __call__(
|
135 |
+
self,
|
136 |
+
to=None,
|
137 |
+
format=None,
|
138 |
+
max_buffer=None,
|
139 |
+
buffer_separator="\n",
|
140 |
+
regex=None,
|
141 |
+
duplicate_out=False,
|
142 |
+
):
|
143 |
+
return _Redirect(
|
144 |
+
self.stdout,
|
145 |
+
self.stderr,
|
146 |
+
format=format,
|
147 |
+
to=to,
|
148 |
+
max_buffer=max_buffer,
|
149 |
+
buffer_separator=buffer_separator,
|
150 |
+
regex=regex,
|
151 |
+
duplicate_out=duplicate_out,
|
152 |
+
)
|
153 |
+
|
154 |
+
def __exit__(self, *exc):
|
155 |
+
if self.active_nested is not None:
|
156 |
+
nested = self.active_nested
|
157 |
+
if nested.active_nested is None:
|
158 |
+
self.active_nested = None
|
159 |
+
return nested.__exit__(*exc)
|
160 |
+
|
161 |
+
res = None
|
162 |
+
for redirection, io_obj in reversed(self.redirections):
|
163 |
+
res = redirection.__exit__(*exc)
|
164 |
+
io_obj.print_at_end()
|
165 |
+
|
166 |
+
self.redirections = []
|
167 |
+
self.st = None
|
168 |
+
self.fun = None
|
169 |
+
return res
|
170 |
+
|
171 |
+
def _write(self, data):
|
172 |
+
self.fun(data)
|
173 |
+
|
174 |
+
|
175 |
+
stdout = _Redirect()
|
176 |
+
stderr = _Redirect(stderr=True)
|
177 |
+
stdouterr = _Redirect(stdout=True, stderr=True)
|
178 |
+
|
179 |
+
"""
|
180 |
+
# can be used as
|
181 |
+
|
182 |
+
import time
|
183 |
+
import sys
|
184 |
+
from random import getrandbits
|
185 |
+
import streamlit.redirect as rd
|
186 |
+
|
187 |
+
st.text('Suboutput:')
|
188 |
+
so = st.empty()
|
189 |
+
|
190 |
+
with rd.stdout, rd.stderr(format='markdown', to=st.sidebar):
|
191 |
+
print("hello ")
|
192 |
+
time.sleep(1)
|
193 |
+
i = 5
|
194 |
+
while i > 0:
|
195 |
+
print("**M**izu? ", file=sys.stdout if getrandbits(1) else sys.stderr)
|
196 |
+
i -= 1
|
197 |
+
with rd.stdout(to=so):
|
198 |
+
print(f" cica {i}")
|
199 |
+
if i:
|
200 |
+
time.sleep(1)
|
201 |
+
# """
|
video_to_images.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
import cv2
|
4 |
+
from tqdm import tqdm
|
5 |
+
|
6 |
+
|
7 |
+
class ImageCreator:
|
8 |
+
def __init__(self, filename, imgs_dir, image_start=0, image_end=0, pbar=True):
|
9 |
+
"""
|
10 |
+
:param str filename: The name of the video's filename.
|
11 |
+
:param str imgs_dir: The directory where to store the image files.
|
12 |
+
:param int image_start: The first image to be extracted.
|
13 |
+
:param int image_end: The last image to be extracted, 0 if full video.
|
14 |
+
:param bool pbar: Whether to display a progress bar.
|
15 |
+
"""
|
16 |
+
|
17 |
+
self.filename = filename
|
18 |
+
self.imgs_dir = imgs_dir
|
19 |
+
self.image_start = image_start
|
20 |
+
self.image_end = image_end
|
21 |
+
self.pbar = pbar
|
22 |
+
if not os.path.exists(imgs_dir):
|
23 |
+
os.makedirs(imgs_dir)
|
24 |
+
|
25 |
+
def get_images(self):
|
26 |
+
vid = cv2.VideoCapture(self.filename)
|
27 |
+
total_frames = int(vid.get(cv2.CAP_PROP_FRAME_COUNT))
|
28 |
+
success, image = vid.read()
|
29 |
+
count = 0
|
30 |
+
if self.image_end == 0:
|
31 |
+
self.image_end = total_frames
|
32 |
+
zfill_max = len(str(total_frames))
|
33 |
+
ok_count = 0
|
34 |
+
print("Writing images...")
|
35 |
+
if self.pbar:
|
36 |
+
pb = tqdm(total=total_frames)
|
37 |
+
while success:
|
38 |
+
if count >= self.image_start and count <= self.image_end:
|
39 |
+
cv2.imwrite(
|
40 |
+
f"{self.imgs_dir}/frame_{str(ok_count).zfill(zfill_max)}.png", image
|
41 |
+
)
|
42 |
+
ok_count += 1
|
43 |
+
success, image = vid.read()
|
44 |
+
if self.pbar:
|
45 |
+
pb.update(1)
|
46 |
+
count += 1
|
47 |
+
if self.pbar:
|
48 |
+
pb.close()
|
49 |
+
print("Wrote {} image files.".format(ok_count))
|