killian31 commited on
Commit
8b09391
·
0 Parent(s):

initial commit

Browse files
.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
+ [![GitHub stars](https://img.shields.io/github/stars/killian31/VideoBackgroundRemoval.svg)](https://github.com/killian31/VideoBackgroundRemoval/stargazers)
4
+ [![black](https://github.com/killian31/VideoBackgroundRemoval/actions/workflows/github-actions-black-formatting.yml/badge.svg)](https://github.com/killian31/VideoBackgroundRemoval/actions/workflows/github-actions-black-formatting.yml)
5
+ [![wakatime](https://wakatime.com/badge/github/killian31/VideoBackgroundRemoval.svg)](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))