Upload 170 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- .github/ISSUE_TEMPLATE/config.yml +8 -0
- .github/ISSUE_TEMPLATE/feature_request.yml +46 -0
- .github/scripts/issue_checker.py +110 -0
- .github/workflows/issue_checker.yaml +23 -0
- .gitignore +12 -0
- CONTRIBUTING.md +3 -0
- LICENSE +0 -0
- README.md +67 -0
- install.py +10 -0
- javascript/deforum-hints.js +214 -0
- javascript/deforum.js +15 -0
- requirements.txt +8 -0
- scripts/deforum.py +22 -0
- scripts/deforum_extend_paths.py +17 -0
- scripts/deforum_helpers/RAFT.py +28 -0
- scripts/deforum_helpers/animation.py +413 -0
- scripts/deforum_helpers/animation_key_frames.py +133 -0
- scripts/deforum_helpers/args.py +1134 -0
- scripts/deforum_helpers/auto_navigation.py +72 -0
- scripts/deforum_helpers/colors.py +22 -0
- scripts/deforum_helpers/composable_masks.py +196 -0
- scripts/deforum_helpers/consistency_check.py +132 -0
- scripts/deforum_helpers/defaults.py +203 -0
- scripts/deforum_helpers/deforum_controlnet.py +324 -0
- scripts/deforum_helpers/deforum_controlnet_gradio.py +34 -0
- scripts/deforum_helpers/deforum_tqdm.py +82 -0
- scripts/deforum_helpers/deprecation_utils.py +82 -0
- scripts/deforum_helpers/depth.py +143 -0
- scripts/deforum_helpers/depth_adabins.py +62 -0
- scripts/deforum_helpers/depth_leres.py +55 -0
- scripts/deforum_helpers/depth_midas.py +75 -0
- scripts/deforum_helpers/depth_zoe.py +30 -0
- scripts/deforum_helpers/frame_interpolation.py +224 -0
- scripts/deforum_helpers/general_utils.py +128 -0
- scripts/deforum_helpers/generate.py +310 -0
- scripts/deforum_helpers/gradio_funcs.py +280 -0
- scripts/deforum_helpers/human_masking.py +70 -0
- scripts/deforum_helpers/hybrid_video.py +594 -0
- scripts/deforum_helpers/image_sharpening.py +22 -0
- scripts/deforum_helpers/load_images.py +96 -0
- scripts/deforum_helpers/masks.py +40 -0
- scripts/deforum_helpers/noise.py +72 -0
- scripts/deforum_helpers/parseq_adapter.py +243 -0
- scripts/deforum_helpers/parseq_adapter_test.py +227 -0
- scripts/deforum_helpers/prompt.py +140 -0
- scripts/deforum_helpers/render.py +615 -0
- scripts/deforum_helpers/render_modes.py +158 -0
- scripts/deforum_helpers/resume.py +56 -0
- scripts/deforum_helpers/rich.py +2 -0
.github/ISSUE_TEMPLATE/bug_report.yml
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Bug Report
|
| 2 |
+
description: Create a bug report for the Deforum extension
|
| 3 |
+
title: "[Bug]: "
|
| 4 |
+
labels: ["bug"]
|
| 5 |
+
|
| 6 |
+
body:
|
| 7 |
+
- type: checkboxes
|
| 8 |
+
attributes:
|
| 9 |
+
label: Have you read the latest version of the FAQ?
|
| 10 |
+
description: Please visit the page called FAQ & Troubleshooting on the Deforum wiki in this repository and see if your problem has already been described there.
|
| 11 |
+
options:
|
| 12 |
+
- label: I have visited the FAQ page right now and my issue is not present there
|
| 13 |
+
required: true
|
| 14 |
+
- type: checkboxes
|
| 15 |
+
attributes:
|
| 16 |
+
label: Is there an existing issue for this?
|
| 17 |
+
description: Please search to see if an issue already exists for the bug you encountered (including the closed issues).
|
| 18 |
+
options:
|
| 19 |
+
- label: I have searched the existing issues and checked the recent builds/commits of both this extension and the webui
|
| 20 |
+
required: true
|
| 21 |
+
- type: checkboxes
|
| 22 |
+
attributes:
|
| 23 |
+
label: Are you using the latest version of the Deforum extension?
|
| 24 |
+
description: Please, check if your Deforum is based on the latest repo commit (git log) or update it through the 'Extensions' tab and check if the issue still persist. Otherwise, check this box.
|
| 25 |
+
options:
|
| 26 |
+
- label: I have Deforum updated to the lastest version and I still have the issue.
|
| 27 |
+
required: true
|
| 28 |
+
- type: markdown
|
| 29 |
+
attributes:
|
| 30 |
+
value: |
|
| 31 |
+
*Please fill this form with as much information as possible, don't forget to fill "What OS..." and *provide screenshots if possible**
|
| 32 |
+
- type: markdown
|
| 33 |
+
attributes:
|
| 34 |
+
value: |
|
| 35 |
+
**Forewarning:* if you won't provide the full crash log, your issue will be discarded*
|
| 36 |
+
- type: textarea
|
| 37 |
+
id: what-did
|
| 38 |
+
attributes:
|
| 39 |
+
label: What happened?
|
| 40 |
+
description: Tell us what happened in a very clear and simple way
|
| 41 |
+
validations:
|
| 42 |
+
required: true
|
| 43 |
+
- type: textarea
|
| 44 |
+
id: steps
|
| 45 |
+
attributes:
|
| 46 |
+
label: Steps to reproduce the problem
|
| 47 |
+
description: Please provide us with precise step by step information on how to reproduce the bug
|
| 48 |
+
value: |
|
| 49 |
+
1. Go to ....
|
| 50 |
+
2. Press ....
|
| 51 |
+
3. ...
|
| 52 |
+
validations:
|
| 53 |
+
required: true
|
| 54 |
+
- type: textarea
|
| 55 |
+
id: what-should
|
| 56 |
+
attributes:
|
| 57 |
+
label: What should have happened/how would you fix it?
|
| 58 |
+
description: Tell what you think the normal behavior should be or any ideas on how to solve it
|
| 59 |
+
- type: textarea
|
| 60 |
+
id: what-torch
|
| 61 |
+
attributes:
|
| 62 |
+
label: Torch version
|
| 63 |
+
description: Which Torch version your WebUI is working with. You can find it by looking at the bottom of the page.
|
| 64 |
+
validations:
|
| 65 |
+
required: true
|
| 66 |
+
- type: dropdown
|
| 67 |
+
id: where
|
| 68 |
+
attributes:
|
| 69 |
+
label: On which platform are you launching the webui with the extension?
|
| 70 |
+
multiple: true
|
| 71 |
+
options:
|
| 72 |
+
- Local PC setup (Windows)
|
| 73 |
+
- Local PC setup (Linux)
|
| 74 |
+
- Local PC setup (Mac)
|
| 75 |
+
- Google Colab (The Last Ben's)
|
| 76 |
+
- Google Colab (Other)
|
| 77 |
+
- Cloud server (Linux)
|
| 78 |
+
- Other (please specify in "additional information")
|
| 79 |
+
- type: textarea
|
| 80 |
+
id: deforumsettings
|
| 81 |
+
attributes:
|
| 82 |
+
label: Deforum settings
|
| 83 |
+
description: Send here a link to your used settings file or the latest generated one in the 'outputs/img2img-images/Deforum/' folder (ideally, upload it to GitHub gists).
|
| 84 |
+
validations:
|
| 85 |
+
required: true
|
| 86 |
+
- type: textarea
|
| 87 |
+
id: customsettings
|
| 88 |
+
attributes:
|
| 89 |
+
label: Webui core settings
|
| 90 |
+
description: Send here a link to your ui-config.json file in the core 'stable-diffusion-webui' folder. Notice, if you have 'With img2img, do exactly the amount of steps the slider specified' checked, your issue will be discarded.
|
| 91 |
+
validations:
|
| 92 |
+
required: true
|
| 93 |
+
- type: textarea
|
| 94 |
+
id: logs
|
| 95 |
+
attributes:
|
| 96 |
+
label: Console logs
|
| 97 |
+
description: Now, it is the most important part which most users fail for the first time! Please provide the **full** cmd/terminal logs from the moment you started the webui (i.e. clicked the launch file or started it from cmd) to the part when your bug happened.
|
| 98 |
+
render: Shell
|
| 99 |
+
validations:
|
| 100 |
+
required: true
|
| 101 |
+
- type: textarea
|
| 102 |
+
id: misc
|
| 103 |
+
attributes:
|
| 104 |
+
label: Additional information
|
| 105 |
+
description: Any relevant additional info or context.
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: false
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: Deforum Github discussions
|
| 4 |
+
url: https://github.com/deforum-art/deforum-for-automatic1111-webui/discussions
|
| 5 |
+
about: Please ask and answer questions here. If you want to complain about something, don't try to circumvent issue filling by starting a discussion here 🙃
|
| 6 |
+
- name: Deforum Discord
|
| 7 |
+
url: https://discord.gg/deforum
|
| 8 |
+
about: Here is our main community where we chat, discuss development and share experiments and results
|
.github/ISSUE_TEMPLATE/feature_request.yml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Feature request
|
| 2 |
+
description: Suggest an idea for the Deforum extension
|
| 3 |
+
title: "[Feature Request]: "
|
| 4 |
+
labels: ["enhancement"]
|
| 5 |
+
|
| 6 |
+
body:
|
| 7 |
+
- type: checkboxes
|
| 8 |
+
attributes:
|
| 9 |
+
label: Is there an existing issue for this?
|
| 10 |
+
description: Please search to see if an issue already exists for the feature you want, and that it's not implemented in a recent build/commit.
|
| 11 |
+
options:
|
| 12 |
+
- label: I have searched the existing issues and checked the recent builds/commits
|
| 13 |
+
required: true
|
| 14 |
+
- type: markdown
|
| 15 |
+
attributes:
|
| 16 |
+
value: |
|
| 17 |
+
*Please fill this form with as much information as possible, provide screenshots and/or illustrations of the feature if possible*
|
| 18 |
+
- type: textarea
|
| 19 |
+
id: feature
|
| 20 |
+
attributes:
|
| 21 |
+
label: What would your feature do ?
|
| 22 |
+
description: Tell us about your feature in a very clear and simple way, and what problem it would solve
|
| 23 |
+
validations:
|
| 24 |
+
required: true
|
| 25 |
+
- type: textarea
|
| 26 |
+
id: workflow
|
| 27 |
+
attributes:
|
| 28 |
+
label: Proposed workflow
|
| 29 |
+
description: Please provide us with step by step information on how you'd like the feature to be accessed and used
|
| 30 |
+
value: |
|
| 31 |
+
1. Go to ....
|
| 32 |
+
2. Press ....
|
| 33 |
+
3. ...
|
| 34 |
+
validations:
|
| 35 |
+
required: true
|
| 36 |
+
- type: textarea
|
| 37 |
+
id: misc
|
| 38 |
+
attributes:
|
| 39 |
+
label: Additional information
|
| 40 |
+
description: Add any other context or screenshots about the feature request here.
|
| 41 |
+
- type: textarea
|
| 42 |
+
attributes:
|
| 43 |
+
label: Are you going to help adding it?
|
| 44 |
+
description: Do you want to participate in Deforum development and bring the desired feature sooner? Let us know if you are willing to add the desired feature, ideally, leave your Discord handle here, so we will contact you for a less formal conversation. Our community is welcoming and ready to provide you with any information on the project structure or how the code works. If not, however, keep in mind that if you do not want to do your new feature yourself, you will have to wait until the team picks up your issue.
|
| 45 |
+
validations:
|
| 46 |
+
required: true
|
.github/scripts/issue_checker.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
from github import Github
|
| 4 |
+
|
| 5 |
+
# Get GitHub token from environment variables
|
| 6 |
+
token = os.environ['GITHUB_TOKEN']
|
| 7 |
+
g = Github(token)
|
| 8 |
+
|
| 9 |
+
# Get the current repository
|
| 10 |
+
print(f"Repo is {os.environ['GITHUB_REPOSITORY']}")
|
| 11 |
+
repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
|
| 12 |
+
|
| 13 |
+
# Get the issue number from the event payload
|
| 14 |
+
#issue_number = int(os.environ['ISSUE_NUMBER'])
|
| 15 |
+
|
| 16 |
+
for issue in repo.get_issues():
|
| 17 |
+
print(f"Processing issue №{issue.number}")
|
| 18 |
+
if issue.pull_request:
|
| 19 |
+
continue
|
| 20 |
+
|
| 21 |
+
# Get the issue object
|
| 22 |
+
#issue = repo.get_issue(issue_number)
|
| 23 |
+
|
| 24 |
+
# Define the keywords to search for in the issue
|
| 25 |
+
keywords = ['Python', 'Commit hash', 'Launching Web UI with arguments', 'Model loaded', 'deforum']
|
| 26 |
+
|
| 27 |
+
# Check if ALL of the keywords are present in the issue
|
| 28 |
+
def check_keywords(issue_body, keywords):
|
| 29 |
+
for keyword in keywords:
|
| 30 |
+
if not re.search(r'\b' + re.escape(keyword) + r'\b', issue_body, re.IGNORECASE):
|
| 31 |
+
return False
|
| 32 |
+
return True
|
| 33 |
+
|
| 34 |
+
# Check if the issue title has at least a specified number of words
|
| 35 |
+
def check_title_word_count(issue_title, min_word_count):
|
| 36 |
+
words = issue_title.replace("/", " ").replace("\\\\", " ").split()
|
| 37 |
+
return len(words) >= min_word_count
|
| 38 |
+
|
| 39 |
+
# Check if the issue title is concise
|
| 40 |
+
def check_title_concise(issue_title, max_word_count):
|
| 41 |
+
words = issue_title.replace("/", " ").replace("\\\\", " ").split()
|
| 42 |
+
return len(words) <= max_word_count
|
| 43 |
+
|
| 44 |
+
# Check if the commit ID is in the correct hash form
|
| 45 |
+
def check_commit_id_format(issue_body):
|
| 46 |
+
match = re.search(r'webui commit id - ([a-fA-F0-9]+|\[[a-fA-F0-9]+\])', issue_body)
|
| 47 |
+
if not match:
|
| 48 |
+
print('webui_commit_id not found')
|
| 49 |
+
return False
|
| 50 |
+
webui_commit_id = match.group(1)
|
| 51 |
+
print(f'webui_commit_id {webui_commit_id}')
|
| 52 |
+
webui_commit_id = webui_commit_id.replace("[", "").replace("]", "")
|
| 53 |
+
if not (7 <= len(webui_commit_id) <= 40):
|
| 54 |
+
print(f'invalid length!')
|
| 55 |
+
return False
|
| 56 |
+
match = re.search(r'deforum exten commit id - ([a-fA-F0-9]+|\[[a-fA-F0-9]+\])', issue_body)
|
| 57 |
+
if match:
|
| 58 |
+
print('deforum commit id not found')
|
| 59 |
+
return False
|
| 60 |
+
t2v_commit_id = match.group(1)
|
| 61 |
+
print(f'deforum_commit_id {t2v_commit_id}')
|
| 62 |
+
t2v_commit_id = t2v_commit_id.replace("[", "").replace("]", "")
|
| 63 |
+
if not (7 <= len(t2v_commit_id) <= 40):
|
| 64 |
+
print(f'invalid length!')
|
| 65 |
+
return False
|
| 66 |
+
return True
|
| 67 |
+
|
| 68 |
+
# Only if a bug report
|
| 69 |
+
if '[Bug]' in issue.title and not '[Feature Request]' in issue.title:
|
| 70 |
+
print('The issue is eligible')
|
| 71 |
+
# Initialize an empty list to store error messages
|
| 72 |
+
error_messages = []
|
| 73 |
+
|
| 74 |
+
# Check for each condition and add the corresponding error message if the condition is not met
|
| 75 |
+
if not check_keywords(issue.body, keywords):
|
| 76 |
+
error_messages.append("Include **THE FULL LOG FROM THE START OF THE WEBUI** in the issue description.")
|
| 77 |
+
|
| 78 |
+
if not check_title_word_count(issue.title, 3):
|
| 79 |
+
error_messages.append("Make sure the issue title has at least 3 words.")
|
| 80 |
+
|
| 81 |
+
if not check_title_concise(issue.title, 13):
|
| 82 |
+
error_messages.append("The issue title should be concise and contain no more than 13 words.")
|
| 83 |
+
|
| 84 |
+
# if not check_commit_id_format(issue.body):
|
| 85 |
+
# error_messages.append("Provide a valid commit ID in the format 'commit id - [commit_hash]' **both** for the WebUI and the Extension.")
|
| 86 |
+
|
| 87 |
+
# If there are any error messages, close the issue and send a comment with the error messages
|
| 88 |
+
if error_messages:
|
| 89 |
+
print('Invalid issue, closing')
|
| 90 |
+
# Add the "not planned" label to the issue
|
| 91 |
+
not_planned_label = repo.get_label("wrong format")
|
| 92 |
+
issue.add_to_labels(not_planned_label)
|
| 93 |
+
|
| 94 |
+
# Close the issue
|
| 95 |
+
issue.edit(state='closed')
|
| 96 |
+
|
| 97 |
+
# Generate the comment by concatenating the error messages
|
| 98 |
+
comment = "This issue has been closed due to incorrect formatting. Please address the following mistakes and reopen the issue (click on the 'Reopen' button below):\n\n"
|
| 99 |
+
comment += "\n".join(f"- {error_message}" for error_message in error_messages)
|
| 100 |
+
|
| 101 |
+
# Add the comment to the issue
|
| 102 |
+
issue.create_comment(comment)
|
| 103 |
+
elif repo.get_label("wrong format") in issue.labels:
|
| 104 |
+
print('Issue is fine')
|
| 105 |
+
issue.edit(state='open')
|
| 106 |
+
issue.delete_labels()
|
| 107 |
+
bug_label = repo.get_label("bug")
|
| 108 |
+
issue.add_to_labels(bug_label)
|
| 109 |
+
comment = "Thanks for addressing your formatting mistakes. The issue has been reopened now."
|
| 110 |
+
issue.create_comment(comment)
|
.github/workflows/issue_checker.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Issue Checker
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
issues:
|
| 5 |
+
types: [opened, reopened, edited]
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
check_issue:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
steps:
|
| 11 |
+
- name: Checkout repository
|
| 12 |
+
uses: actions/checkout@v3
|
| 13 |
+
- name: Set up Python
|
| 14 |
+
uses: actions/setup-python@v3
|
| 15 |
+
with:
|
| 16 |
+
python-version: '3.x'
|
| 17 |
+
- name: Install dependencies
|
| 18 |
+
run: pip install PyGithub
|
| 19 |
+
- name: Check issue
|
| 20 |
+
env:
|
| 21 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 22 |
+
ISSUE_NUMBER: ${{ github.event.number }}
|
| 23 |
+
run: python .github/scripts/issue_checker.py
|
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unnecessary compiled python files.
|
| 2 |
+
__pycache__
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
|
| 6 |
+
# Output Images
|
| 7 |
+
outputs
|
| 8 |
+
|
| 9 |
+
# Log files for colab-convert
|
| 10 |
+
cc-outputs.log
|
| 11 |
+
*.safetensors
|
| 12 |
+
scripts/deforum_helpers/navigation.py
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing
|
| 2 |
+
|
| 3 |
+
When contributing please ping the devs via Discord https://discord.gg/deforum to make sure you addition will fit well such a large project and to get help if needed.
|
LICENSE
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Deforum Stable Diffusion — official extension for AUTOMATIC1111's webui
|
| 3 |
+
|
| 4 |
+
<p align="left">
|
| 5 |
+
<a href="https://github.com/deforum-art/sd-webui-deforum/commits"><img alt="Last Commit" src="https://img.shields.io/github/last-commit/deforum-art/deforum-for-automatic1111-webui"></a>
|
| 6 |
+
<a href="https://github.com/deforum-art/sd-webui-deforum/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/deforum-art/deforum-for-automatic1111-webui"></a>
|
| 7 |
+
<a href="https://github.com/deforum-art/sd-webui-deforum/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/deforum-art/deforum-for-automatic1111-webui"></a>
|
| 8 |
+
<a href="https://github.com/deforum-art/sd-webui-deforum/network"><img alt="GitHub forks" src="https://img.shields.io/github/forks/deforum-art/deforum-for-automatic1111-webui"></a>
|
| 9 |
+
</a>
|
| 10 |
+
</p>
|
| 11 |
+
|
| 12 |
+
## Need help? See our [FAQ](https://github.com/deforum-art/sd-webui-deforum/wiki/FAQ-&-Troubleshooting)
|
| 13 |
+
|
| 14 |
+
## Getting Started
|
| 15 |
+
|
| 16 |
+
1. Install [AUTOMATIC1111's webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui/).
|
| 17 |
+
|
| 18 |
+
2. Now two ways: either clone the repo into the `extensions` directory via git commandline launched within in the `stable-diffusion-webui` folder
|
| 19 |
+
|
| 20 |
+
```sh
|
| 21 |
+
git clone https://github.com/deforum-art/sd-webui-deforum extensions/deforum
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Or download this repository, locate the `extensions` folder within your WebUI installation, create a folder named `deforum` and put the contents of the downloaded directory inside of it. Then restart WebUI.
|
| 25 |
+
|
| 26 |
+
3. Open the webui, find the Deforum tab at the top of the page.
|
| 27 |
+
|
| 28 |
+
4. Enter the animation settings. Refer to [this general guide](https://docs.google.com/document/d/1pEobUknMFMkn8F5TMsv8qRzamXX_75BShMMXV8IFslI/edit) and [this guide to math keyframing functions in Deforum](https://docs.google.com/document/d/1pfW1PwbDIuW0cv-dnuyYj1UzPqe23BlSLTJsqazffXM/edit?usp=sharing). However, **in this version prompt weights less than zero don't just like in original Deforum!** Split the positive and the negative prompt in the json section using --neg argument like this "apple:\`where(cos(t)>=0, cos(t), 0)\`, snow --neg strawberry:\`where(cos(t)<0, -cos(t), 0)\`"
|
| 29 |
+
|
| 30 |
+
5. To view animation frames as they're being made, without waiting for the completion of an animation, go to the 'Settings' tab and set the value of this toolbar **above zero**. Warning: it may slow down the generation process.
|
| 31 |
+
|
| 32 |
+

|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
6. Run the script and see if you got it working or even got something. **In 3D mode a large delay is expected at first** as the script loads the depth models. In the end, using the default settings the whole thing should consume 6.4 GBs of VRAM at 3D mode peaks and no more than 3.8 GB VRAM in 3D mode if you launch the webui with the '--lowvram' command line argument.
|
| 36 |
+
|
| 37 |
+
7. After the generation process is completed, click the button with the self-describing name to show the video or gif result right in the GUI!
|
| 38 |
+
|
| 39 |
+
8. Join our Discord where you can post generated stuff, ask questions and more: https://discord.gg/deforum. <br>
|
| 40 |
+
* There's also the 'Issues' tab in the repo, for well... reporting issues ;)
|
| 41 |
+
|
| 42 |
+
9. Profit!
|
| 43 |
+
|
| 44 |
+
## Known issues
|
| 45 |
+
|
| 46 |
+
* This port is not fully backward-compatible with the notebook and the local version both due to the changes in how AUTOMATIC1111's webui handles Stable Diffusion models and the changes in this script to get it to work in the new environment. *Expect* that you may not get exactly the same result or that the thing may break down because of the older settings.
|
| 47 |
+
|
| 48 |
+
## Screenshots
|
| 49 |
+
|
| 50 |
+
Amazing raw Deforum animation by [Pxl.Pshr](https://www.instagram.com/pxl.pshr):
|
| 51 |
+
* Turn Audio ON!
|
| 52 |
+
|
| 53 |
+
(Audio credits: SKRILLEX, FRED AGAIN & FLOWDAN - RUMBLE (PHACE'S DNB FLIP))
|
| 54 |
+
|
| 55 |
+
https://user-images.githubusercontent.com/121192995/224450647-39529b28-be04-4871-bb7a-faf7afda2ef2.mp4
|
| 56 |
+
|
| 57 |
+
Setting file of that video: [here](https://github.com/deforum-art/sd-webui-deforum/files/11353167/PxlPshrWinningAnimationSettings.txt).
|
| 58 |
+
|
| 59 |
+
<br>
|
| 60 |
+
|
| 61 |
+
Main extension tab:
|
| 62 |
+
|
| 63 |
+

|
| 64 |
+
|
| 65 |
+
Keyframes tab:
|
| 66 |
+
|
| 67 |
+

|
install.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import launch
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
req_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt")
|
| 5 |
+
|
| 6 |
+
with open(req_file) as file:
|
| 7 |
+
for lib in file:
|
| 8 |
+
lib = lib.strip()
|
| 9 |
+
if not launch.is_installed(lib):
|
| 10 |
+
launch.run_pip(f"install {lib}", f"Deforum requirement: {lib}")
|
javascript/deforum-hints.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// mouseover tooltips for various UI elements
|
| 2 |
+
|
| 3 |
+
deforum_titles = {
|
| 4 |
+
//Run
|
| 5 |
+
"Override settings": "specify a custom settings file and ignore settings displayed in the interface",
|
| 6 |
+
"Custom settings file": "the path to a custom settings file",
|
| 7 |
+
"Width": "The width of the output images, in pixels (must be a multiple of 64)",
|
| 8 |
+
"Height": "The height of the output images, in pixels (must be a multiple of 64)",
|
| 9 |
+
"Restore faces": "Restore low quality faces using GFPGAN neural network",
|
| 10 |
+
"Tiling": "Produce an image that can be tiled.",
|
| 11 |
+
"Highres. fix": "Use a two step process to partially create an image at smaller resolution, upscale, and then improve details in it without changing composition",
|
| 12 |
+
"Seed": "A value that determines the output of random number generator - if you create an image with same parameters and seed as another image, you'll get the same result",
|
| 13 |
+
"Sampler": "Which algorithm to use to produce the image",
|
| 14 |
+
"Enable extras": "enable additional seed settings",
|
| 15 |
+
"Subseed": "Seed of a different picture to be mixed into the generation.",
|
| 16 |
+
"Subseed strength": "How strong of a variation to produce. At 0, there will be no effect. At 1, you will get the complete picture with variation seed (except for ancestral samplers, where you will just get something).",
|
| 17 |
+
"Resize seed from width": "Normally, changing the resolution will completely change an image, even when using the same seed. If you generated an image with a particular seed and then changed the resolution, put the original resolution here to get an image that more closely resemles the original",
|
| 18 |
+
"Resize seed from height": "Normally, changing the resolution will completely change an image, even when using the same seed. If you generated an image with a particular seed and then changed the resolution, put the original resolution here to get an image that more closely resemles the original",
|
| 19 |
+
"Steps": "How many times to improve the generated image iteratively; higher values take longer; very low values can produce bad results",
|
| 20 |
+
"Batch name": "output images will be placed in a folder with this name ({timestring} token will be replaced) inside the img2img output folder. Supports placeholders like {seed}, {w}, {h}, {prompts} and more",
|
| 21 |
+
"Pix2Pix img CFG schedule": "*Only in use with pix2pix checkpoints!*",
|
| 22 |
+
"Filename format": "specify the format of the filename for output images",
|
| 23 |
+
"Seed behavior": "defines the seed behavior that is used for animations",
|
| 24 |
+
"iter": "the seed value will increment by 1 for each subsequent frame of the animation",
|
| 25 |
+
"fixed": "the seed will remain fixed across all frames of animation. **NOT RECOMMENDED.** Unless you know what you are doing, it will *deep fry* the pictures over time",
|
| 26 |
+
"random": "a random seed will be used on each frame of the animation",
|
| 27 |
+
"schedule": "specify your own seed schedule",
|
| 28 |
+
"Seed iter N":"controls for how many frames the same seed should stick before iterating to the next one",
|
| 29 |
+
//Keyframes
|
| 30 |
+
"Animation mode": "selects the type of animation",
|
| 31 |
+
"2D": "only 2D motion parameters will be used, but this mode uses the least amount of VRAM. You can optionally enable flip_2d_perspective to enable some psuedo-3d animation parameters while in 2D mode.",
|
| 32 |
+
"3D": "enables all 3D motion parameters.",
|
| 33 |
+
"Video Input": "will ignore all motion parameters and attempt to reference a video loaded into the runtime, specified by the video_init_path. Max_frames is ignored during video_input mode, and instead, follows the number of frames pulled from the video’s length. Resume_from_timestring is NOT available with Video_Input mode.",
|
| 34 |
+
"Max frames": "the maximum number of output images to be created",
|
| 35 |
+
"Border": "controls handling method of pixels to be generated when the image is smaller than the frame.",
|
| 36 |
+
"wrap": "pulls pixels from the opposite edge of the image",
|
| 37 |
+
"replicate": "repeats the edge of the pixels, and extends them. Animations with quick motion may yield lines where this border function was attempting to populate pixels into the empty space created.",
|
| 38 |
+
"Zoom": "2D operator that scales the canvas size, multiplicatively. [static = 1.0]",
|
| 39 |
+
"Angle": "2D operator to rotate canvas clockwise/anticlockwise in degrees per frame",
|
| 40 |
+
"Transform Center X": "x center axis for 2D angle/zoom *only*",
|
| 41 |
+
"Transform Center Y": "y center axis for 2D angle/zoom *only*",
|
| 42 |
+
"Translation X": "2D & 3D operator to move canvas left/right in pixels per frame",
|
| 43 |
+
"Translation Y": "2D & 3D operator to move canvas up/down in pixels per frame",
|
| 44 |
+
"Translation Z": "3D operator to move canvas towards/away from view [speed set by FOV]",
|
| 45 |
+
"Rotation 3D X": "3D operator to tilt canvas up/down in degrees per frame",
|
| 46 |
+
"Rotation 3D Y": "3D operator to pan canvas left/right in degrees per frame",
|
| 47 |
+
"Rotation 3D Z": "3D operator to roll canvas clockwise/anticlockwise",
|
| 48 |
+
"Enable perspective flip": "enables 2D mode functions to simulate faux 3D movement",
|
| 49 |
+
"Perspective flip theta": "the roll effect angle",
|
| 50 |
+
"Perspective flip phi": "the tilt effect angle",
|
| 51 |
+
"Perspective flip gamma": "the pan effect angle",
|
| 52 |
+
"Perspective flip fv": "the 2D vanishing point of perspective (recommended range 30-160)",
|
| 53 |
+
"Noise schedule": "amount of graininess to add per frame for diffusion diversity",
|
| 54 |
+
"Strength schedule": "amount of presence of previous frame to influence next frame, also controls steps in the following formula [steps - (strength_schedule * steps)]",
|
| 55 |
+
"Sampler schedule": "controls which sampler to use at a specific scheduled frame",
|
| 56 |
+
"Contrast schedule": "adjusts the overall contrast per frame [default neutral at 1.0]",
|
| 57 |
+
"CFG scale schedule": "how closely the image should conform to the prompt. Lower values produce more creative results. (recommended range 5-15)",
|
| 58 |
+
"FOV schedule": "adjusts the scale at which the canvas is moved in 3D by the translation_z value. [maximum range -180 to +180, with 0 being undefined. Values closer to 180 will make the image have less depth, while values closer to 0 will allow more depth]",
|
| 59 |
+
"Aspect Ratio schedule": "adjusts the aspect ratio for the depth calculation (normally 1)",
|
| 60 |
+
//"near_schedule": "",
|
| 61 |
+
//"far_schedule": "",
|
| 62 |
+
"Seed schedule": "allows you to specify seeds at a specific schedule, if seed_behavior is set to schedule.",
|
| 63 |
+
"Color coherence": "The color coherence will attempt to sample the overall pixel color information, and trend those values analyzed in the first frame to be applied to future frames.",
|
| 64 |
+
// "None": "Disable color coherence",
|
| 65 |
+
"HSV": "HSV is a good method for balancing presence of vibrant colors, but may produce unrealistic results - (ie.blue apples)",
|
| 66 |
+
"LAB": "LAB is a more linear approach to mimic human perception of color space - a good default setting for most users.",
|
| 67 |
+
"RGB": "RGB is good for enforcing unbiased amounts of color in each red, green and blue channel - some images may yield colorized artifacts if sampling is too low.",
|
| 68 |
+
"Legacy colormatch": "applies the colormatch only before the video noising, resulting in graying the video over time, use it for backwards compatibility",
|
| 69 |
+
"Cadence": "A setting of 1 will cause every frame to receive diffusion in the sequence of image outputs. A setting of 2 will only diffuse on every other frame, yet motion will still be in effect. The output of images during the cadence sequence will be automatically blended, additively and saved to the specified drive. This may improve the illusion of coherence in some workflows as the content and context of an image will not change or diffuse during frames that were skipped. Higher values of 4-8 cadence will skip over a larger amount of frames and only diffuse the “Nth” frame as set by the diffusion_cadence value. This may produce more continuity in an animation, at the cost of little opportunity to add more diffused content. In extreme examples, motion within a frame will fail to produce diverse prompt context, and the space will be filled with lines or approximations of content - resulting in unexpected animation patterns and artifacts. Video Input & Interpolation modes are not affected by diffusion_cadence.",
|
| 70 |
+
"Optical flow cadence": "Optional method for optical flow used to blend frames during cadence in 3D animation mode (if cadence more than 1).",
|
| 71 |
+
"Optical flow redo generation": "This option takes twice as long because it generates twice in order to capture the optical flow from the previous image to the first generation, then warps the previous image and redoes the generation. Works in 2D/3D animation modes.",
|
| 72 |
+
"Redo": "Diffusion Redo. This option renders N times before the final render. It is suggested to lower your steps if you up your redo. Seed is randomized during redo generations and restored afterwards.",
|
| 73 |
+
"Noise type": "Selects the type of noise being added to each frame",
|
| 74 |
+
"uniform": "Uniform noise covers the entire frame. It somewhat flattens and sharpens the video over time, but may be good for cartoonish look. This is the old default setting.",
|
| 75 |
+
"perlin": "Perlin noise is a more natural looking noise. It is heterogeneous and less sharp than uniform noise, this way it is more likely that new details will appear in a more coherent way. This is the new default setting.",
|
| 76 |
+
"Perlin W": "The width of the Perlin sample. Lower values will make larger noise regions. Think of it as inverse brush stroke width. The greater this setting, the smaller details it will affect.",
|
| 77 |
+
"Perlin H": "The height of the Perlin sample. Lower values will make larger noise regions. Think of it as inverse brush stroke width. The greater this setting, the smaller details it will affect.",
|
| 78 |
+
"Perlin octaves": "The number of Perlin noise octaves, that is the count of P-noise iterations. Higher values will make the noise more soft and smoke-like, whereas lower values will make it look more organic and spotty. It is limited by 8 octaves as the resulting gain will run out of bounds.",
|
| 79 |
+
"Perlin persistence": "How much of noise from each octave is added on each iteration. Higher values will make it more straighter and sharper, while lower values will make it rounder and smoother. It is limited by 1.0 as the resulting gain fill the frame completely with noise.",
|
| 80 |
+
"Use depth warping": "enables instructions to warp an image dynamically in 3D mode only.",
|
| 81 |
+
"MiDaS weight": "sets a midpoint at which a depthmap is to be drawn: range [-1 to +1]",
|
| 82 |
+
"Padding mode": "instructs the handling of pixels outside the field of view as they come into the scene.",
|
| 83 |
+
//"border": "Border will attempt to use the edges of the canvas as the pixels to be drawn", //duplicate name as another property
|
| 84 |
+
"reflection": "reflection will attempt to approximate the image and tile/repeat pixels",
|
| 85 |
+
"zeros": "zeros will not add any new pixel information",
|
| 86 |
+
"Sampling Mode": "choose from Bicubic, Bilinear or Nearest modes. (Recommended: Bicubic)",
|
| 87 |
+
"Save depth maps": "will output a greyscale depth map image alongside the output images.",
|
| 88 |
+
|
| 89 |
+
// Prompts
|
| 90 |
+
"Prompts": "prompts for your animation in a JSON format. Use --neg words to add 'words' as negative prompt",
|
| 91 |
+
"Prompts positive": "positive prompt to be appended to *all* prompts",
|
| 92 |
+
"Prompts negative": "negative prompt to be appended to *all* prompts. DON'T use --neg here!",
|
| 93 |
+
|
| 94 |
+
//Init
|
| 95 |
+
"Use init": "Diffuse the first frame based on an image, similar to img2img.",
|
| 96 |
+
"Strength": "Controls the strength of the diffusion on the init image. 0 = disabled",
|
| 97 |
+
"Strength 0 no init": "Set the strength to 0 automatically when no init image is used",
|
| 98 |
+
"Init image": "the path to your init image",
|
| 99 |
+
"Use mask": "Use a grayscale image as a mask on your init image. Whiter areas of the mask are areas that change more.",
|
| 100 |
+
"Use alpha as mask": "use the alpha channel of the init image as the mask",
|
| 101 |
+
"Mask file": "the path to your mask image",
|
| 102 |
+
"Invert mask": "Inverts the colors of the mask",
|
| 103 |
+
"Mask brightness adjust": "adjust the brightness of the mask. Should be a positive number, with 1.0 meaning no adjustment.",
|
| 104 |
+
"Mask contrast adjust": "adjust the brightness of the mask. Should be a positive number, with 1.0 meaning no adjustment.",
|
| 105 |
+
"overlay mask": "Overlay the masked image at the end of the generation so it does not get degraded by encoding and decoding",
|
| 106 |
+
"Mask overlay blur": "Blur edges of final overlay mask, if used. Minimum = 0 (no blur)",
|
| 107 |
+
"Video init path": "the directory \/ URL at which your video file is located for Video Input mode only",
|
| 108 |
+
"Extract nth frame": "during the run sequence, only frames specified by this value will be extracted, saved, and diffused upon. A value of 1 indicates that every frame is to be accounted for. Values of 2 will use every other frame for the sequence. Higher values will skip that number of frames respectively.",
|
| 109 |
+
"Extract from frame":"start extracting the input video only from this frame number",
|
| 110 |
+
"Extract to frame": "stop the extraction of the video at this frame number. -1 for no limits",
|
| 111 |
+
"Overwrite extracted frames": "when enabled, will re-extract video frames each run. When using video_input mode, the run will be instructed to write video frames to the drive. If you’ve already populated the frames needed, uncheck this box to skip past redundant extraction, and immediately start the render. If you have not extracted frames, you must run at least once with this box checked to write the necessary frames.",
|
| 112 |
+
"Use mask video": "video_input mode only, enables the extraction and use of a separate video file intended for use as a mask. White areas of the extracted video frames will not be affected by diffusion, while black areas will be fully effected. Lighter/darker areas are affected dynamically.",
|
| 113 |
+
"Video mask path": "the directory in which your mask video is located.",
|
| 114 |
+
"Interpolate key frames": "selects whether to ignore prompt schedule or _x_frames.",
|
| 115 |
+
"Interpolate x frames": "the number of frames to transition thru between prompts (when interpolate_key_frames = true, then the numbers in front of the animation prompts will dynamically guide the images based on their value. If set to false, will ignore the prompt numbers and force interpole_x_frames value regardless of prompt number)",
|
| 116 |
+
"Resume from timestring": "instructs the run to start from a specified point",
|
| 117 |
+
"Resume timestring": "the required timestamp to reference when resuming. Currently only available in 2D & 3D mode, the timestamp is saved as the settings .txt file name as well as images produced during your previous run. The format follows: yyyymmddhhmmss - a timestamp of when the run was started to diffuse.",
|
| 118 |
+
|
| 119 |
+
//Video Output
|
| 120 |
+
"Skip video creation": "when checked, do not output a video",
|
| 121 |
+
"Make GIF": "create a gif in addition to .mp4 file. supports up to 30 fps, will self-disable at higher fps values",
|
| 122 |
+
"Upscale":"upscale the images of the next run once it's finished + make a video out of them",
|
| 123 |
+
"Upscale model":"model of the upscaler to use. 'realesr-animevideov3' is much faster but yields smoother, less detailed results. the other models only do x4",
|
| 124 |
+
"Upscale factor":"how many times to upscale, actual options depend on the chosen upscale model",
|
| 125 |
+
"FPS": "The frames per second that the video will run at",
|
| 126 |
+
"Output format": "select the type of video file to output",
|
| 127 |
+
"PIL gif": "create an animated GIF",
|
| 128 |
+
"FFMPEG mp4": "create an MP4 video file",
|
| 129 |
+
"FFmpeg location": "the path to where ffmpeg is located. Leave at default 'ffmpeg' if ffmpeg is in your PATH!",
|
| 130 |
+
"FFmpeg crf": "controls quality where lower is better, less compressed. values: 0 to 51, default 17",
|
| 131 |
+
"FFmpeg preset": "controls how good the compression is, and the operation speed. If you're not in a rush keep it at 'veryslow'",
|
| 132 |
+
"Add soundtrack": "when this box is checked, and FFMPEG mp4 is selected as the output format, an audio file will be multiplexed with the video.",
|
| 133 |
+
"Soundtrack path": "the path\/ URL to an audio file to accompany the video",
|
| 134 |
+
"Use manual settings": "when this is unchecked, the video will automatically be created in the same output folder as the images. Check this box to specify different settings for the creation of the video, specified by the following options",
|
| 135 |
+
"Render steps": "render each step of diffusion as a separate frame",
|
| 136 |
+
"Max video frames": "the maximum number of frames to include in the video, when use_manual_settings is checked",
|
| 137 |
+
"Image path": "the location of images to create the video from, when use_manual_settings is checked",
|
| 138 |
+
"MP4 path": "the output location of the mp4 file, when use_manual_settings is checked",
|
| 139 |
+
"Delete Imgs": "if enabled, raw imgs will be deleted after a successful video/ videos (upsacling, interpolation, gif) creation",
|
| 140 |
+
"Engine": "choose the frame interpolation engine and version",
|
| 141 |
+
"Interp X":"how many times to interpolate the source video. e.g source video fps of 12 and a value of x2 will yield a 24fps interpolated video",
|
| 142 |
+
"Slow-Mo X":"how many times to slow-down the video. *Naturally affects output fps as well",
|
| 143 |
+
"Keep Imgs": "delete or keep raw affected (interpolated/ upscaled depending on the UI section) png imgs",
|
| 144 |
+
"Interpolate an existing video":"This feature allows you to interpolate any video with a dedicated button. Video could be completly unrelated to deforum",
|
| 145 |
+
"In Frame Count": "uploaded video total frame count",
|
| 146 |
+
"In FPS":"uploaded video FPS",
|
| 147 |
+
"Interpolated Vid FPS":"calculated output-interpolated video FPS",
|
| 148 |
+
"In Res":"uploaded video resolution",
|
| 149 |
+
"Out Res":"output video resolution",
|
| 150 |
+
|
| 151 |
+
// Looper Args
|
| 152 |
+
// "use_looper": "",
|
| 153 |
+
"Enable guided images mode": "check this box to enable guided images mode",
|
| 154 |
+
"Images to use for keyframe guidance": "images you iterate over, you can do local or web paths (no single backslashes!)",
|
| 155 |
+
"Image strength schedule": "how much the image should look like the previou one and new image frame init. strength schedule might be better if this is higher, around .75 during the keyfames you want to switch on",
|
| 156 |
+
"Blend factor max": "blendFactor = blendFactorMax - blendFactorSlope * cos((frame % tweening_frames_schedule) / (tweening_frames_schedule / 2))",
|
| 157 |
+
"Blend factor slope": "blendFactor = blendFactorMax - blendFactorSlope * cos((frame % tweening_frames_schedule) / (tweening_frames_schedule / 2))",
|
| 158 |
+
"Tweening frames schedule": "number of the frames that we will blend between current imagined image and input frame image",
|
| 159 |
+
"Color correction factor": "how close to get to the colors of the input frame image/ the amount each frame during a tweening step to use the new images colors",
|
| 160 |
+
// deforum.py / right side of the ui:
|
| 161 |
+
"Settings File": "Path to settings file you want to load. Path can be relative to webui folder OR full - absolute",
|
| 162 |
+
|
| 163 |
+
// Hybrid Video
|
| 164 |
+
"Generate inputframes": "Initiates extraction of video frames from your video_init_path to the inputframes folder. You only need to do this once and then you can change it to False and re-render",
|
| 165 |
+
"Hybrid composite": "Engages hybrid compositing of video into animation in various ways with comp alpha as a master mix control.",
|
| 166 |
+
"Use init image as video": "Use init image instead of video. Doesn't require generation of inputframes.",
|
| 167 |
+
"First Frame as init image": "If True, uses the first frame of the video as the init_image. False can create interesting transition effects into the video, depending on settings.",
|
| 168 |
+
"Motion use prev img": "If enabled, changes the behavior or hybrid_motion to captures motion by comparing the current video frame to the previous rendered image, instead of the previous video frame.",
|
| 169 |
+
"Hybrid motion": "Analyzes video frames for camera motion and applies movement to render.",
|
| 170 |
+
"Flow method": "Selects the type of Optical Flow to use if Optical Flow is selected in Hybrid motion.",
|
| 171 |
+
"Comp mask type": "You don't need a mask to composite video. But, Mask types can control the way that video is composited with the previous image each frame.",
|
| 172 |
+
"Comp mask equalize": "Equalizes the mask for the composite before or after autocontrast operation (or both)",
|
| 173 |
+
"Comp mask auto contrast": "Auto-contrasts the mask for the composite. If enabled, uses the low/high autocontrast cutoff schedules.",
|
| 174 |
+
"Comp mask inverse": "Inverts the composite mask.",
|
| 175 |
+
"Comp save extra frames": "If this option is selected, many extra frames will be output for the various processes into the hybridframes folder.",
|
| 176 |
+
"Comp alpha schedule": "Schedule controls how much the composite video is mixed in, whether set to mask is None or using a mask. This is the master mix.",
|
| 177 |
+
"Flow factor schedule": "Affects optical flow hybrid motion. 1 is normal flow. -1 is negative flow. 0.5 is half flow, etc...",
|
| 178 |
+
"Comp mask blend alpha schedule": "If using a blend mask, this controls the blend amount of the video and render for the composite mask.",
|
| 179 |
+
"Comp mask contrast schedule": "Controls the contrast of the composite mask. 0.5 if half, 1 is normal contrast, 2 is double, etc.",
|
| 180 |
+
"Comp mask auto contrast cutoff high schedule": "If using autocontrast option, this is the high cutoff for the operation.",
|
| 181 |
+
"Comp mask auto contrast cutoff low schedule": "If using autocontrast option, this is the low cutoff for the operation.",
|
| 182 |
+
"Generate human masks": "This will generate masks of all the humans in a video. Created at generation of hybrid video. Not yet integrated for auto-masking, but it will create the masks, and you can then use the mask video manually.",
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
onUiUpdate(function(){
|
| 186 |
+
gradioApp().querySelectorAll('span, button, select, p').forEach(function(span){
|
| 187 |
+
tooltip = deforum_titles[span.textContent];
|
| 188 |
+
|
| 189 |
+
if(!tooltip){
|
| 190 |
+
tooltip = deforum_titles[span.value];
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
if(!tooltip){
|
| 194 |
+
for (const c of span.classList) {
|
| 195 |
+
if (c in deforum_titles) {
|
| 196 |
+
tooltip = deforum_titles[c];
|
| 197 |
+
break;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
if(tooltip){
|
| 203 |
+
span.title = tooltip;
|
| 204 |
+
}
|
| 205 |
+
})
|
| 206 |
+
|
| 207 |
+
gradioApp().querySelectorAll('select').forEach(function(select){
|
| 208 |
+
if (select.onchange != null) return;
|
| 209 |
+
|
| 210 |
+
select.onchange = function(){
|
| 211 |
+
select.title = deforum_titles[select.value] || "";
|
| 212 |
+
}
|
| 213 |
+
})
|
| 214 |
+
})
|
javascript/deforum.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function submit_deforum(){
|
| 2 |
+
rememberGallerySelection('deforum_gallery')
|
| 3 |
+
showSubmitButtons('deforum', false)
|
| 4 |
+
|
| 5 |
+
var id = randomId()
|
| 6 |
+
requestProgress(id, gradioApp().getElementById('deforum_gallery_container'), gradioApp().getElementById('deforum_gallery'), function(){
|
| 7 |
+
showSubmitButtons('deforum', true)
|
| 8 |
+
})
|
| 9 |
+
|
| 10 |
+
var res = create_submit_args(arguments)
|
| 11 |
+
|
| 12 |
+
res[0] = id
|
| 13 |
+
|
| 14 |
+
return res
|
| 15 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numexpr
|
| 2 |
+
matplotlib
|
| 3 |
+
pandas
|
| 4 |
+
av
|
| 5 |
+
pims
|
| 6 |
+
imageio_ffmpeg
|
| 7 |
+
rich
|
| 8 |
+
gdown
|
scripts/deforum.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from modules import script_callbacks
|
| 3 |
+
import modules.paths as ph
|
| 4 |
+
from scripts.deforum_extend_paths import deforum_sys_extend
|
| 5 |
+
|
| 6 |
+
def init_deforum():
|
| 7 |
+
# use sys.path.extend to make sure all of our files are available for importation
|
| 8 |
+
deforum_sys_extend()
|
| 9 |
+
|
| 10 |
+
# create the Models/Deforum folder, where many of the deforum related models/ packages will be downloaded
|
| 11 |
+
os.makedirs(ph.models_path + '/Deforum', exist_ok=True)
|
| 12 |
+
|
| 13 |
+
# import our on_ui_tabs and on_ui_settings functions from the respected files
|
| 14 |
+
from deforum_helpers.ui_right import on_ui_tabs
|
| 15 |
+
from deforum_helpers.ui_settings import on_ui_settings
|
| 16 |
+
|
| 17 |
+
# trigger webui's extensions mechanism using our imported main functions -
|
| 18 |
+
# first to create the actual deforum gui, then to make the deforum tab in webui's settings section
|
| 19 |
+
script_callbacks.on_ui_tabs(on_ui_tabs)
|
| 20 |
+
script_callbacks.on_ui_settings(on_ui_settings)
|
| 21 |
+
|
| 22 |
+
init_deforum()
|
scripts/deforum_extend_paths.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
def deforum_sys_extend():
|
| 5 |
+
deforum_folder_name = os.path.sep.join(os.path.abspath(__file__).split(os.path.sep)[:-2])
|
| 6 |
+
|
| 7 |
+
basedirs = [os.getcwd()]
|
| 8 |
+
if 'google.colab' in sys.modules:
|
| 9 |
+
basedirs.append('/content/gdrive/MyDrive/sd/stable-diffusion-webui') # for TheLastBen's colab
|
| 10 |
+
for _ in basedirs:
|
| 11 |
+
deforum_paths_to_ensure = [
|
| 12 |
+
os.path.join(deforum_folder_name, 'scripts'),
|
| 13 |
+
os.path.join(deforum_folder_name, 'scripts', 'deforum_helpers', 'src')
|
| 14 |
+
]
|
| 15 |
+
for deforum_scripts_path_fix in deforum_paths_to_ensure:
|
| 16 |
+
if deforum_scripts_path_fix not in sys.path:
|
| 17 |
+
sys.path.extend([deforum_scripts_path_fix])
|
scripts/deforum_helpers/RAFT.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torchvision.transforms.functional as F
|
| 4 |
+
from torchvision.models.optical_flow import Raft_Large_Weights, raft_large
|
| 5 |
+
|
| 6 |
+
class RAFT:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
weights = Raft_Large_Weights.DEFAULT
|
| 9 |
+
self.transforms = weights.transforms()
|
| 10 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 11 |
+
self.model = raft_large(weights=weights, progress=False).to(self.device).eval()
|
| 12 |
+
|
| 13 |
+
def predict(self, image1, image2, num_flow_updates:int = 50):
|
| 14 |
+
img1 = F.to_tensor(image1)
|
| 15 |
+
img2 = F.to_tensor(image2)
|
| 16 |
+
img1_batch, img2_batch = img1.unsqueeze(0), img2.unsqueeze(0)
|
| 17 |
+
img1_batch, img2_batch = self.transforms(img1_batch, img2_batch)
|
| 18 |
+
|
| 19 |
+
with torch.no_grad():
|
| 20 |
+
flow = self.model(image1=img1_batch.to(self.device), image2=img2_batch.to(self.device), num_flow_updates=num_flow_updates)[-1].cpu().numpy()[0]
|
| 21 |
+
|
| 22 |
+
# align the flow array to have the shape (w, h, 2) so it's compatible with the rest of CV2's flow methods
|
| 23 |
+
flow = np.transpose(flow, (1, 2, 0))
|
| 24 |
+
|
| 25 |
+
return flow
|
| 26 |
+
|
| 27 |
+
def delete_model(self):
|
| 28 |
+
del self.model
|
scripts/deforum_helpers/animation.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import cv2
|
| 3 |
+
import py3d_tools as p3d # this is actually a file in our /src folder!
|
| 4 |
+
from functools import reduce
|
| 5 |
+
import math
|
| 6 |
+
import torch
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
from modules.shared import state, opts
|
| 9 |
+
from .prompt import check_is_number
|
| 10 |
+
from .general_utils import debug_print
|
| 11 |
+
|
| 12 |
+
def sample_from_cv2(sample: np.ndarray) -> torch.Tensor:
|
| 13 |
+
sample = ((sample.astype(float) / 255.0) * 2) - 1
|
| 14 |
+
sample = sample[None].transpose(0, 3, 1, 2).astype(np.float16)
|
| 15 |
+
sample = torch.from_numpy(sample)
|
| 16 |
+
return sample
|
| 17 |
+
|
| 18 |
+
def sample_to_cv2(sample: torch.Tensor, type=np.uint8) -> np.ndarray:
|
| 19 |
+
sample_f32 = rearrange(sample.squeeze().cpu().numpy(), "c h w -> h w c").astype(np.float32)
|
| 20 |
+
sample_f32 = ((sample_f32 * 0.5) + 0.5).clip(0, 1)
|
| 21 |
+
sample_int8 = (sample_f32 * 255)
|
| 22 |
+
return sample_int8.astype(type)
|
| 23 |
+
|
| 24 |
+
def construct_RotationMatrixHomogenous(rotation_angles):
|
| 25 |
+
assert(type(rotation_angles)==list and len(rotation_angles)==3)
|
| 26 |
+
RH = np.eye(4,4)
|
| 27 |
+
cv2.Rodrigues(np.array(rotation_angles), RH[0:3, 0:3])
|
| 28 |
+
return RH
|
| 29 |
+
|
| 30 |
+
# https://en.wikipedia.org/wiki/Rotation_matrix
|
| 31 |
+
def getRotationMatrixManual(rotation_angles):
|
| 32 |
+
|
| 33 |
+
rotation_angles = [np.deg2rad(x) for x in rotation_angles]
|
| 34 |
+
|
| 35 |
+
phi = rotation_angles[0] # around x
|
| 36 |
+
gamma = rotation_angles[1] # around y
|
| 37 |
+
theta = rotation_angles[2] # around z
|
| 38 |
+
|
| 39 |
+
# X rotation
|
| 40 |
+
Rphi = np.eye(4,4)
|
| 41 |
+
sp = np.sin(phi)
|
| 42 |
+
cp = np.cos(phi)
|
| 43 |
+
Rphi[1,1] = cp
|
| 44 |
+
Rphi[2,2] = Rphi[1,1]
|
| 45 |
+
Rphi[1,2] = -sp
|
| 46 |
+
Rphi[2,1] = sp
|
| 47 |
+
|
| 48 |
+
# Y rotation
|
| 49 |
+
Rgamma = np.eye(4,4)
|
| 50 |
+
sg = np.sin(gamma)
|
| 51 |
+
cg = np.cos(gamma)
|
| 52 |
+
Rgamma[0,0] = cg
|
| 53 |
+
Rgamma[2,2] = Rgamma[0,0]
|
| 54 |
+
Rgamma[0,2] = sg
|
| 55 |
+
Rgamma[2,0] = -sg
|
| 56 |
+
|
| 57 |
+
# Z rotation (in-image-plane)
|
| 58 |
+
Rtheta = np.eye(4,4)
|
| 59 |
+
st = np.sin(theta)
|
| 60 |
+
ct = np.cos(theta)
|
| 61 |
+
Rtheta[0,0] = ct
|
| 62 |
+
Rtheta[1,1] = Rtheta[0,0]
|
| 63 |
+
Rtheta[0,1] = -st
|
| 64 |
+
Rtheta[1,0] = st
|
| 65 |
+
|
| 66 |
+
R = reduce(lambda x,y : np.matmul(x,y), [Rphi, Rgamma, Rtheta])
|
| 67 |
+
|
| 68 |
+
return R
|
| 69 |
+
|
| 70 |
+
def getPoints_for_PerspectiveTranformEstimation(ptsIn, ptsOut, W, H, sidelength):
|
| 71 |
+
|
| 72 |
+
ptsIn2D = ptsIn[0,:]
|
| 73 |
+
ptsOut2D = ptsOut[0,:]
|
| 74 |
+
ptsOut2Dlist = []
|
| 75 |
+
ptsIn2Dlist = []
|
| 76 |
+
|
| 77 |
+
for i in range(0,4):
|
| 78 |
+
ptsOut2Dlist.append([ptsOut2D[i,0], ptsOut2D[i,1]])
|
| 79 |
+
ptsIn2Dlist.append([ptsIn2D[i,0], ptsIn2D[i,1]])
|
| 80 |
+
|
| 81 |
+
pin = np.array(ptsIn2Dlist) + [W/2.,H/2.]
|
| 82 |
+
pout = (np.array(ptsOut2Dlist) + [1.,1.]) * (0.5*sidelength)
|
| 83 |
+
pin = pin.astype(np.float32)
|
| 84 |
+
pout = pout.astype(np.float32)
|
| 85 |
+
|
| 86 |
+
return pin, pout
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def warpMatrix(W, H, theta, phi, gamma, scale, fV):
|
| 90 |
+
|
| 91 |
+
# M is to be estimated
|
| 92 |
+
M = np.eye(4, 4)
|
| 93 |
+
|
| 94 |
+
fVhalf = np.deg2rad(fV/2.)
|
| 95 |
+
d = np.sqrt(W*W+H*H)
|
| 96 |
+
sideLength = scale*d/np.cos(fVhalf)
|
| 97 |
+
h = d/(2.0*np.sin(fVhalf))
|
| 98 |
+
n = h-(d/2.0)
|
| 99 |
+
f = h+(d/2.0)
|
| 100 |
+
|
| 101 |
+
# Translation along Z-axis by -h
|
| 102 |
+
T = np.eye(4,4)
|
| 103 |
+
T[2,3] = -h
|
| 104 |
+
|
| 105 |
+
# Rotation matrices around x,y,z
|
| 106 |
+
R = getRotationMatrixManual([phi, gamma, theta])
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# Projection Matrix
|
| 110 |
+
P = np.eye(4,4)
|
| 111 |
+
P[0,0] = 1.0/np.tan(fVhalf)
|
| 112 |
+
P[1,1] = P[0,0]
|
| 113 |
+
P[2,2] = -(f+n)/(f-n)
|
| 114 |
+
P[2,3] = -(2.0*f*n)/(f-n)
|
| 115 |
+
P[3,2] = -1.0
|
| 116 |
+
|
| 117 |
+
# pythonic matrix multiplication
|
| 118 |
+
F = reduce(lambda x,y : np.matmul(x,y), [P, T, R])
|
| 119 |
+
|
| 120 |
+
# shape should be 1,4,3 for ptsIn and ptsOut since perspectiveTransform() expects data in this way.
|
| 121 |
+
# In C++, this can be achieved by Mat ptsIn(1,4,CV_64FC3);
|
| 122 |
+
ptsIn = np.array([[
|
| 123 |
+
[-W/2., H/2., 0.],[ W/2., H/2., 0.],[ W/2.,-H/2., 0.],[-W/2.,-H/2., 0.]
|
| 124 |
+
]])
|
| 125 |
+
ptsOut = np.array(np.zeros((ptsIn.shape), dtype=ptsIn.dtype))
|
| 126 |
+
ptsOut = cv2.perspectiveTransform(ptsIn, F)
|
| 127 |
+
|
| 128 |
+
ptsInPt2f, ptsOutPt2f = getPoints_for_PerspectiveTranformEstimation(ptsIn, ptsOut, W, H, sideLength)
|
| 129 |
+
|
| 130 |
+
# check float32 otherwise OpenCV throws an error
|
| 131 |
+
assert(ptsInPt2f.dtype == np.float32)
|
| 132 |
+
assert(ptsOutPt2f.dtype == np.float32)
|
| 133 |
+
M33 = cv2.getPerspectiveTransform(ptsInPt2f,ptsOutPt2f)
|
| 134 |
+
|
| 135 |
+
return M33, sideLength
|
| 136 |
+
|
| 137 |
+
def get_flip_perspective_matrix(W, H, keys, frame_idx):
|
| 138 |
+
perspective_flip_theta = keys.perspective_flip_theta_series[frame_idx]
|
| 139 |
+
perspective_flip_phi = keys.perspective_flip_phi_series[frame_idx]
|
| 140 |
+
perspective_flip_gamma = keys.perspective_flip_gamma_series[frame_idx]
|
| 141 |
+
perspective_flip_fv = keys.perspective_flip_fv_series[frame_idx]
|
| 142 |
+
M,sl = warpMatrix(W, H, perspective_flip_theta, perspective_flip_phi, perspective_flip_gamma, 1., perspective_flip_fv);
|
| 143 |
+
post_trans_mat = np.float32([[1, 0, (W-sl)/2], [0, 1, (H-sl)/2]])
|
| 144 |
+
post_trans_mat = np.vstack([post_trans_mat, [0,0,1]])
|
| 145 |
+
bM = np.matmul(M, post_trans_mat)
|
| 146 |
+
return bM
|
| 147 |
+
|
| 148 |
+
def flip_3d_perspective(anim_args, prev_img_cv2, keys, frame_idx):
|
| 149 |
+
W, H = (prev_img_cv2.shape[1], prev_img_cv2.shape[0])
|
| 150 |
+
return cv2.warpPerspective(
|
| 151 |
+
prev_img_cv2,
|
| 152 |
+
get_flip_perspective_matrix(W, H, keys, frame_idx),
|
| 153 |
+
(W, H),
|
| 154 |
+
borderMode=cv2.BORDER_WRAP if anim_args.border == 'wrap' else cv2.BORDER_REPLICATE
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
def anim_frame_warp(prev_img_cv2, args, anim_args, keys, frame_idx, depth_model=None, depth=None, device='cuda', half_precision = False):
|
| 158 |
+
|
| 159 |
+
if anim_args.use_depth_warping:
|
| 160 |
+
if depth is None and depth_model is not None:
|
| 161 |
+
depth = depth_model.predict(prev_img_cv2, anim_args.midas_weight, half_precision)
|
| 162 |
+
|
| 163 |
+
else:
|
| 164 |
+
depth = None
|
| 165 |
+
|
| 166 |
+
if anim_args.animation_mode == '2D':
|
| 167 |
+
prev_img = anim_frame_warp_2d(prev_img_cv2, args, anim_args, keys, frame_idx)
|
| 168 |
+
else: # '3D'
|
| 169 |
+
prev_img = anim_frame_warp_3d(device, prev_img_cv2, depth, anim_args, keys, frame_idx)
|
| 170 |
+
|
| 171 |
+
return prev_img, depth
|
| 172 |
+
|
| 173 |
+
def anim_frame_warp_2d(prev_img_cv2, args, anim_args, keys, frame_idx):
|
| 174 |
+
angle = keys.angle_series[frame_idx]
|
| 175 |
+
zoom = keys.zoom_series[frame_idx]
|
| 176 |
+
translation_x = keys.translation_x_series[frame_idx]
|
| 177 |
+
translation_y = keys.translation_y_series[frame_idx]
|
| 178 |
+
transform_center_x = keys.transform_center_x_series[frame_idx]
|
| 179 |
+
transform_center_y = keys.transform_center_y_series[frame_idx]
|
| 180 |
+
center_point = (args.W * transform_center_x, args.H * transform_center_y)
|
| 181 |
+
rot_mat = cv2.getRotationMatrix2D(center_point, angle, zoom)
|
| 182 |
+
trans_mat = np.float32([[1, 0, translation_x], [0, 1, translation_y]])
|
| 183 |
+
trans_mat = np.vstack([trans_mat, [0,0,1]])
|
| 184 |
+
rot_mat = np.vstack([rot_mat, [0,0,1]])
|
| 185 |
+
if anim_args.enable_perspective_flip:
|
| 186 |
+
bM = get_flip_perspective_matrix(args.W, args.H, keys, frame_idx)
|
| 187 |
+
rot_mat = np.matmul(bM, rot_mat, trans_mat)
|
| 188 |
+
else:
|
| 189 |
+
rot_mat = np.matmul(rot_mat, trans_mat)
|
| 190 |
+
return cv2.warpPerspective(
|
| 191 |
+
prev_img_cv2,
|
| 192 |
+
rot_mat,
|
| 193 |
+
(prev_img_cv2.shape[1], prev_img_cv2.shape[0]),
|
| 194 |
+
borderMode=cv2.BORDER_WRAP if anim_args.border == 'wrap' else cv2.BORDER_REPLICATE
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
def anim_frame_warp_3d(device, prev_img_cv2, depth, anim_args, keys, frame_idx):
|
| 198 |
+
TRANSLATION_SCALE = 1.0/200.0 # matches Disco
|
| 199 |
+
translate_xyz = [
|
| 200 |
+
-keys.translation_x_series[frame_idx] * TRANSLATION_SCALE,
|
| 201 |
+
keys.translation_y_series[frame_idx] * TRANSLATION_SCALE,
|
| 202 |
+
-keys.translation_z_series[frame_idx] * TRANSLATION_SCALE
|
| 203 |
+
]
|
| 204 |
+
rotate_xyz = [
|
| 205 |
+
math.radians(keys.rotation_3d_x_series[frame_idx]),
|
| 206 |
+
math.radians(keys.rotation_3d_y_series[frame_idx]),
|
| 207 |
+
math.radians(keys.rotation_3d_z_series[frame_idx])
|
| 208 |
+
]
|
| 209 |
+
if anim_args.enable_perspective_flip:
|
| 210 |
+
prev_img_cv2 = flip_3d_perspective(anim_args, prev_img_cv2, keys, frame_idx)
|
| 211 |
+
rot_mat = p3d.euler_angles_to_matrix(torch.tensor(rotate_xyz, device=device), "XYZ").unsqueeze(0)
|
| 212 |
+
result = transform_image_3d_switcher(device if not device.type.startswith('mps') else torch.device('cpu'), prev_img_cv2, depth, rot_mat, translate_xyz, anim_args, keys, frame_idx)
|
| 213 |
+
torch.cuda.empty_cache()
|
| 214 |
+
return result
|
| 215 |
+
|
| 216 |
+
def transform_image_3d_switcher(device, prev_img_cv2, depth_tensor, rot_mat, translate, anim_args, keys, frame_idx):
|
| 217 |
+
if anim_args.depth_algorithm.lower() in ['midas+adabins (old)', 'zoe+adabins (old)']:
|
| 218 |
+
return transform_image_3d_legacy(device, prev_img_cv2, depth_tensor, rot_mat, translate, anim_args, keys, frame_idx)
|
| 219 |
+
else:
|
| 220 |
+
return transform_image_3d_new(device, prev_img_cv2, depth_tensor, rot_mat, translate, anim_args, keys, frame_idx)
|
| 221 |
+
|
| 222 |
+
def transform_image_3d_legacy(device, prev_img_cv2, depth_tensor, rot_mat, translate, anim_args, keys, frame_idx):
|
| 223 |
+
# adapted and optimized version of transform_image_3d from Disco Diffusion https://github.com/alembics/disco-diffusion
|
| 224 |
+
w, h = prev_img_cv2.shape[1], prev_img_cv2.shape[0]
|
| 225 |
+
|
| 226 |
+
if anim_args.aspect_ratio_use_old_formula:
|
| 227 |
+
aspect_ratio = float(w)/float(h)
|
| 228 |
+
else:
|
| 229 |
+
aspect_ratio = keys.aspect_ratio_series[frame_idx]
|
| 230 |
+
|
| 231 |
+
near = keys.near_series[frame_idx]
|
| 232 |
+
far = keys.far_series[frame_idx]
|
| 233 |
+
fov_deg = keys.fov_series[frame_idx]
|
| 234 |
+
persp_cam_old = p3d.FoVPerspectiveCameras(near, far, aspect_ratio, fov=fov_deg, degrees=True, device=device)
|
| 235 |
+
persp_cam_new = p3d.FoVPerspectiveCameras(near, far, aspect_ratio, fov=fov_deg, degrees=True, R=rot_mat, T=torch.tensor([translate]), device=device)
|
| 236 |
+
|
| 237 |
+
# range of [-1,1] is important to torch grid_sample's padding handling
|
| 238 |
+
y,x = torch.meshgrid(torch.linspace(-1.,1.,h,dtype=torch.float32,device=device),torch.linspace(-1.,1.,w,dtype=torch.float32,device=device))
|
| 239 |
+
if depth_tensor is None:
|
| 240 |
+
z = torch.ones_like(x)
|
| 241 |
+
else:
|
| 242 |
+
z = torch.as_tensor(depth_tensor, dtype=torch.float32, device=device)
|
| 243 |
+
xyz_old_world = torch.stack((x.flatten(), y.flatten(), z.flatten()), dim=1)
|
| 244 |
+
|
| 245 |
+
xyz_old_cam_xy = persp_cam_old.get_full_projection_transform().transform_points(xyz_old_world)[:,0:2]
|
| 246 |
+
xyz_new_cam_xy = persp_cam_new.get_full_projection_transform().transform_points(xyz_old_world)[:,0:2]
|
| 247 |
+
|
| 248 |
+
offset_xy = xyz_new_cam_xy - xyz_old_cam_xy
|
| 249 |
+
# affine_grid theta param expects a batch of 2D mats. Each is 2x3 to do rotation+translation.
|
| 250 |
+
identity_2d_batch = torch.tensor([[1.,0.,0.],[0.,1.,0.]], device=device).unsqueeze(0)
|
| 251 |
+
# coords_2d will have shape (N,H,W,2).. which is also what grid_sample needs.
|
| 252 |
+
coords_2d = torch.nn.functional.affine_grid(identity_2d_batch, [1,1,h,w], align_corners=False)
|
| 253 |
+
offset_coords_2d = coords_2d - torch.reshape(offset_xy, (h,w,2)).unsqueeze(0)
|
| 254 |
+
|
| 255 |
+
image_tensor = rearrange(torch.from_numpy(prev_img_cv2.astype(np.float32)), 'h w c -> c h w').to(device)
|
| 256 |
+
new_image = torch.nn.functional.grid_sample(
|
| 257 |
+
image_tensor.add(1/512 - 0.0001).unsqueeze(0),
|
| 258 |
+
offset_coords_2d,
|
| 259 |
+
mode=anim_args.sampling_mode,
|
| 260 |
+
padding_mode=anim_args.padding_mode,
|
| 261 |
+
align_corners=False
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# convert back to cv2 style numpy array
|
| 265 |
+
result = rearrange(
|
| 266 |
+
new_image.squeeze().clamp(0,255),
|
| 267 |
+
'c h w -> h w c'
|
| 268 |
+
).cpu().numpy().astype(prev_img_cv2.dtype)
|
| 269 |
+
return result
|
| 270 |
+
|
| 271 |
+
def transform_image_3d_new(device, prev_img_cv2, depth_tensor, rot_mat, translate, anim_args, keys, frame_idx):
|
| 272 |
+
'''
|
| 273 |
+
originally an adapted and optimized version of transform_image_3d from Disco Diffusion https://github.com/alembics/disco-diffusion
|
| 274 |
+
modified by reallybigname to control various incoming tensors
|
| 275 |
+
'''
|
| 276 |
+
if anim_args.depth_algorithm.lower().startswith('midas'): # 'Midas-3-Hybrid' or 'Midas-3.1-BeitLarge'
|
| 277 |
+
depth = 1
|
| 278 |
+
depth_factor = -1
|
| 279 |
+
depth_offset = -2
|
| 280 |
+
elif anim_args.depth_algorithm.lower() == "adabins":
|
| 281 |
+
depth = 1
|
| 282 |
+
depth_factor = 1
|
| 283 |
+
depth_offset = 1
|
| 284 |
+
elif anim_args.depth_algorithm.lower() == "leres":
|
| 285 |
+
depth = 1
|
| 286 |
+
depth_factor = 1
|
| 287 |
+
depth_offset = 1
|
| 288 |
+
elif anim_args.depth_algorithm.lower() == "zoe":
|
| 289 |
+
depth = 1
|
| 290 |
+
depth_factor = 1
|
| 291 |
+
depth_offset = 1
|
| 292 |
+
else:
|
| 293 |
+
raise Exception(f"Unknown depth_algorithm passed to transform_image_3d function: {anim_args.depth_algorithm}")
|
| 294 |
+
|
| 295 |
+
w, h = prev_img_cv2.shape[1], prev_img_cv2.shape[0]
|
| 296 |
+
|
| 297 |
+
# depth stretching aspect ratio (has nothing to do with image dimensions - which is why the old formula was flawed)
|
| 298 |
+
aspect_ratio = float(w)/float(h) if anim_args.aspect_ratio_use_old_formula else keys.aspect_ratio_series[frame_idx]
|
| 299 |
+
|
| 300 |
+
# get projection keys
|
| 301 |
+
near = keys.near_series[frame_idx]
|
| 302 |
+
far = keys.far_series[frame_idx]
|
| 303 |
+
fov_deg = keys.fov_series[frame_idx]
|
| 304 |
+
|
| 305 |
+
# get perspective cams old (still) and new (transformed)
|
| 306 |
+
persp_cam_old = p3d.FoVPerspectiveCameras(near, far, aspect_ratio, fov=fov_deg, degrees=True, device=device)
|
| 307 |
+
persp_cam_new = p3d.FoVPerspectiveCameras(near, far, aspect_ratio, fov=fov_deg, degrees=True, R=rot_mat, T=torch.tensor([translate]), device=device)
|
| 308 |
+
|
| 309 |
+
# make xy meshgrid - range of [-1,1] is important to torch grid_sample's padding handling
|
| 310 |
+
y,x = torch.meshgrid(torch.linspace(-1.,1.,h,dtype=torch.float32,device=device),torch.linspace(-1.,1.,w,dtype=torch.float32,device=device))
|
| 311 |
+
|
| 312 |
+
# test tensor for validity (some are corrupted for some reason)
|
| 313 |
+
depth_tensor_invalid = depth_tensor is None or torch.isnan(depth_tensor).any() or torch.isinf(depth_tensor).any() or depth_tensor.min() == depth_tensor.max()
|
| 314 |
+
|
| 315 |
+
if depth_tensor is not None:
|
| 316 |
+
debug_print(f"Depth_T.min: {depth_tensor.min()}, Depth_T.max: {depth_tensor.max()}")
|
| 317 |
+
# if invalid, create flat z for this frame
|
| 318 |
+
if depth_tensor_invalid:
|
| 319 |
+
# if none, then 3D depth is turned off, so no warning is needed.
|
| 320 |
+
if depth_tensor is not None:
|
| 321 |
+
print("Depth tensor invalid. Generating a Flat depth for this frame.")
|
| 322 |
+
# create flat depth
|
| 323 |
+
z = torch.ones_like(x)
|
| 324 |
+
# create z from depth tensor
|
| 325 |
+
else:
|
| 326 |
+
# prepare tensor between 0 and 1 with optional equalization and autocontrast
|
| 327 |
+
depth_normalized = prepare_depth_tensor(depth_tensor)
|
| 328 |
+
|
| 329 |
+
# Rescale the depth values to depth with offset (depth 2 and offset -1 would be -1 to +11)
|
| 330 |
+
depth_final = depth_normalized * depth + depth_offset
|
| 331 |
+
|
| 332 |
+
# depth factor (1 is normal. -1 is inverted)
|
| 333 |
+
if depth_factor != 1:
|
| 334 |
+
depth_final *= depth_factor
|
| 335 |
+
|
| 336 |
+
# console reporting of depth normalization, min, max, diff
|
| 337 |
+
# will *only* print to console if Dev mode is enabled in general settings of Deforum
|
| 338 |
+
txt_depth_min, txt_depth_max = '{:.2f}'.format(float(depth_tensor.min())), '{:.2f}'.format(float(depth_tensor.max()))
|
| 339 |
+
diff = '{:.2f}'.format(float(depth_tensor.max()) - float(depth_tensor.min()))
|
| 340 |
+
console_txt = f"\033[36mDepth normalized to {depth_final.min()}/{depth_final.max()} from"
|
| 341 |
+
debug_print(f"{console_txt} {txt_depth_min}/{txt_depth_max} diff {diff}\033[0m")
|
| 342 |
+
|
| 343 |
+
# add z from depth
|
| 344 |
+
z = torch.as_tensor(depth_final, dtype=torch.float32, device=device)
|
| 345 |
+
|
| 346 |
+
# calculate offset_xy
|
| 347 |
+
xyz_old_world = torch.stack((x.flatten(), y.flatten(), z.flatten()), dim=1)
|
| 348 |
+
xyz_old_cam_xy = persp_cam_old.get_full_projection_transform().transform_points(xyz_old_world)[:,0:2]
|
| 349 |
+
xyz_new_cam_xy = persp_cam_new.get_full_projection_transform().transform_points(xyz_old_world)[:,0:2]
|
| 350 |
+
offset_xy = xyz_new_cam_xy - xyz_old_cam_xy
|
| 351 |
+
|
| 352 |
+
# affine_grid theta param expects a batch of 2D mats. Each is 2x3 to do rotation+translation.
|
| 353 |
+
identity_2d_batch = torch.tensor([[1.,0.,0.],[0.,1.,0.]], device=device).unsqueeze(0)
|
| 354 |
+
|
| 355 |
+
# coords_2d will have shape (N,H,W,2).. which is also what grid_sample needs.
|
| 356 |
+
coords_2d = torch.nn.functional.affine_grid(identity_2d_batch, [1,1,h,w], align_corners=False)
|
| 357 |
+
offset_coords_2d = coords_2d - torch.reshape(offset_xy, (h,w,2)).unsqueeze(0)
|
| 358 |
+
|
| 359 |
+
# do the hyperdimensional remap
|
| 360 |
+
image_tensor = rearrange(torch.from_numpy(prev_img_cv2.astype(np.float32)), 'h w c -> c h w').to(device)
|
| 361 |
+
new_image = torch.nn.functional.grid_sample(
|
| 362 |
+
image_tensor.unsqueeze(0), # image_tensor.add(1/512 - 0.0001).unsqueeze(0),
|
| 363 |
+
offset_coords_2d,
|
| 364 |
+
mode=anim_args.sampling_mode,
|
| 365 |
+
padding_mode=anim_args.padding_mode,
|
| 366 |
+
align_corners=False
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# convert back to cv2 style numpy array
|
| 370 |
+
result = rearrange(
|
| 371 |
+
new_image.squeeze().clamp(0,255),
|
| 372 |
+
'c h w -> h w c'
|
| 373 |
+
).cpu().numpy().astype(prev_img_cv2.dtype)
|
| 374 |
+
return result
|
| 375 |
+
|
| 376 |
+
def prepare_depth_tensor(depth_tensor=None):
|
| 377 |
+
# Prepares a depth tensor with normalization & equalization between 0 and 1
|
| 378 |
+
depth_range = depth_tensor.max() - depth_tensor.min()
|
| 379 |
+
depth_tensor = (depth_tensor - depth_tensor.min()) / depth_range
|
| 380 |
+
depth_tensor = depth_equalization(depth_tensor=depth_tensor)
|
| 381 |
+
return depth_tensor
|
| 382 |
+
|
| 383 |
+
def depth_equalization(depth_tensor):
|
| 384 |
+
"""
|
| 385 |
+
Perform histogram equalization on a single-channel depth tensor.
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
depth_tensor (torch.Tensor): A 2D depth tensor (H, W).
|
| 389 |
+
|
| 390 |
+
Returns:
|
| 391 |
+
torch.Tensor: Equalized depth tensor (2D).
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
# Convert the depth tensor to a NumPy array for processing
|
| 395 |
+
depth_array = depth_tensor.cpu().numpy()
|
| 396 |
+
|
| 397 |
+
# Calculate the histogram of the depth values using a specified number of bins
|
| 398 |
+
# Increase the number of bins for higher precision depth tensors
|
| 399 |
+
hist, bin_edges = np.histogram(depth_array, bins=1024, range=(0, 1))
|
| 400 |
+
|
| 401 |
+
# Calculate the cumulative distribution function (CDF) of the histogram
|
| 402 |
+
cdf = hist.cumsum()
|
| 403 |
+
|
| 404 |
+
# Normalize the CDF so that the maximum value is 1
|
| 405 |
+
cdf = cdf / float(cdf[-1])
|
| 406 |
+
|
| 407 |
+
# Perform histogram equalization by mapping the original depth values to the CDF values
|
| 408 |
+
equalized_depth_array = np.interp(depth_array, bin_edges[:-1], cdf)
|
| 409 |
+
|
| 410 |
+
# Convert the equalized depth array back to a PyTorch tensor and return it
|
| 411 |
+
equalized_depth_tensor = torch.from_numpy(equalized_depth_array).to(depth_tensor.device)
|
| 412 |
+
|
| 413 |
+
return equalized_depth_tensor
|
scripts/deforum_helpers/animation_key_frames.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import numpy as np
|
| 3 |
+
import numexpr
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from .prompt import check_is_number
|
| 6 |
+
|
| 7 |
+
class DeformAnimKeys():
|
| 8 |
+
def __init__(self, anim_args, seed=-1):
|
| 9 |
+
self.fi = FrameInterpolater(anim_args.max_frames, seed)
|
| 10 |
+
self.angle_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.angle))
|
| 11 |
+
self.transform_center_x_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.transform_center_x))
|
| 12 |
+
self.transform_center_y_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.transform_center_y))
|
| 13 |
+
self.zoom_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.zoom))
|
| 14 |
+
self.translation_x_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.translation_x))
|
| 15 |
+
self.translation_y_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.translation_y))
|
| 16 |
+
self.translation_z_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.translation_z))
|
| 17 |
+
self.rotation_3d_x_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.rotation_3d_x))
|
| 18 |
+
self.rotation_3d_y_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.rotation_3d_y))
|
| 19 |
+
self.rotation_3d_z_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.rotation_3d_z))
|
| 20 |
+
self.perspective_flip_theta_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.perspective_flip_theta))
|
| 21 |
+
self.perspective_flip_phi_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.perspective_flip_phi))
|
| 22 |
+
self.perspective_flip_gamma_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.perspective_flip_gamma))
|
| 23 |
+
self.perspective_flip_fv_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.perspective_flip_fv))
|
| 24 |
+
self.noise_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.noise_schedule))
|
| 25 |
+
self.strength_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.strength_schedule))
|
| 26 |
+
self.contrast_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.contrast_schedule))
|
| 27 |
+
self.cfg_scale_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.cfg_scale_schedule))
|
| 28 |
+
self.ddim_eta_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.ddim_eta_schedule))
|
| 29 |
+
self.ancestral_eta_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.ancestral_eta_schedule))
|
| 30 |
+
self.pix2pix_img_cfg_scale_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.pix2pix_img_cfg_scale_schedule))
|
| 31 |
+
self.subseed_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.subseed_schedule))
|
| 32 |
+
self.subseed_strength_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.subseed_strength_schedule))
|
| 33 |
+
self.checkpoint_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.checkpoint_schedule), is_single_string = True)
|
| 34 |
+
self.steps_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.steps_schedule))
|
| 35 |
+
self.seed_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.seed_schedule))
|
| 36 |
+
self.sampler_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.sampler_schedule), is_single_string = True)
|
| 37 |
+
self.clipskip_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.clipskip_schedule))
|
| 38 |
+
self.noise_multiplier_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.noise_multiplier_schedule))
|
| 39 |
+
self.mask_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.mask_schedule), is_single_string = True)
|
| 40 |
+
self.noise_mask_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.noise_mask_schedule), is_single_string = True)
|
| 41 |
+
self.kernel_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.kernel_schedule))
|
| 42 |
+
self.sigma_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.sigma_schedule))
|
| 43 |
+
self.amount_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.amount_schedule))
|
| 44 |
+
self.threshold_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.threshold_schedule))
|
| 45 |
+
self.aspect_ratio_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.aspect_ratio_schedule))
|
| 46 |
+
self.fov_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.fov_schedule))
|
| 47 |
+
self.near_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.near_schedule))
|
| 48 |
+
self.cadence_flow_factor_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.cadence_flow_factor_schedule))
|
| 49 |
+
self.redo_flow_factor_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.redo_flow_factor_schedule))
|
| 50 |
+
self.far_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.far_schedule))
|
| 51 |
+
self.hybrid_comp_alpha_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_comp_alpha_schedule))
|
| 52 |
+
self.hybrid_comp_mask_blend_alpha_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_comp_mask_blend_alpha_schedule))
|
| 53 |
+
self.hybrid_comp_mask_contrast_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_comp_mask_contrast_schedule))
|
| 54 |
+
self.hybrid_comp_mask_auto_contrast_cutoff_high_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_comp_mask_auto_contrast_cutoff_high_schedule))
|
| 55 |
+
self.hybrid_comp_mask_auto_contrast_cutoff_low_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_comp_mask_auto_contrast_cutoff_low_schedule))
|
| 56 |
+
self.hybrid_flow_factor_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(anim_args.hybrid_flow_factor_schedule))
|
| 57 |
+
|
| 58 |
+
class ControlNetKeys():
|
| 59 |
+
def __init__(self, anim_args, controlnet_args):
|
| 60 |
+
self.fi = FrameInterpolater(max_frames=anim_args.max_frames)
|
| 61 |
+
self.schedules = {}
|
| 62 |
+
for i in range(1, 6): # 5 CN models in total
|
| 63 |
+
for suffix in ['weight', 'guidance_start', 'guidance_end']:
|
| 64 |
+
prefix = f"cn_{i}"
|
| 65 |
+
key = f"{prefix}_{suffix}_schedule_series"
|
| 66 |
+
self.schedules[key] = self.fi.get_inbetweens(self.fi.parse_key_frames(getattr(controlnet_args, f"{prefix}_{suffix}")))
|
| 67 |
+
setattr(self, key, self.schedules[key])
|
| 68 |
+
|
| 69 |
+
class LooperAnimKeys():
|
| 70 |
+
def __init__(self, loop_args, anim_args, seed):
|
| 71 |
+
self.fi = FrameInterpolater(anim_args.max_frames, seed)
|
| 72 |
+
self.use_looper = loop_args.use_looper
|
| 73 |
+
self.imagesToKeyframe = loop_args.init_images
|
| 74 |
+
self.image_strength_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(loop_args.image_strength_schedule))
|
| 75 |
+
self.blendFactorMax_series = self.fi.get_inbetweens(self.fi.parse_key_frames(loop_args.blendFactorMax))
|
| 76 |
+
self.blendFactorSlope_series = self.fi.get_inbetweens(self.fi.parse_key_frames(loop_args.blendFactorSlope))
|
| 77 |
+
self.tweening_frames_schedule_series = self.fi.get_inbetweens(self.fi.parse_key_frames(loop_args.tweening_frames_schedule))
|
| 78 |
+
self.color_correction_factor_series = self.fi.get_inbetweens(self.fi.parse_key_frames(loop_args.color_correction_factor))
|
| 79 |
+
|
| 80 |
+
class FrameInterpolater():
|
| 81 |
+
def __init__(self, max_frames=0, seed=-1) -> None:
|
| 82 |
+
self.max_frames = max_frames
|
| 83 |
+
self.seed = seed
|
| 84 |
+
|
| 85 |
+
def sanitize_value(self, value):
|
| 86 |
+
return value.replace("'","").replace('"',"").replace('(',"").replace(')',"")
|
| 87 |
+
|
| 88 |
+
def get_inbetweens(self, key_frames, integer=False, interp_method='Linear', is_single_string = False):
|
| 89 |
+
key_frame_series = pd.Series([np.nan for a in range(self.max_frames)])
|
| 90 |
+
# get our ui variables set for numexpr.evaluate
|
| 91 |
+
max_f = self.max_frames -1
|
| 92 |
+
s = self.seed
|
| 93 |
+
for i in range(0, self.max_frames):
|
| 94 |
+
if i in key_frames:
|
| 95 |
+
value = key_frames[i]
|
| 96 |
+
value_is_number = check_is_number(self.sanitize_value(value))
|
| 97 |
+
if value_is_number: # if it's only a number, leave the rest for the default interpolation
|
| 98 |
+
key_frame_series[i] = self.sanitize_value(value)
|
| 99 |
+
if not value_is_number:
|
| 100 |
+
t = i
|
| 101 |
+
# workaround for values formatted like 0:("I am test") //used for sampler schedules
|
| 102 |
+
key_frame_series[i] = numexpr.evaluate(value) if not is_single_string else self.sanitize_value(value)
|
| 103 |
+
elif is_single_string:# take previous string value and replicate it
|
| 104 |
+
key_frame_series[i] = key_frame_series[i-1]
|
| 105 |
+
key_frame_series = key_frame_series.astype(float) if not is_single_string else key_frame_series # as string
|
| 106 |
+
|
| 107 |
+
if interp_method == 'Cubic' and len(key_frames.items()) <= 3:
|
| 108 |
+
interp_method = 'Quadratic'
|
| 109 |
+
if interp_method == 'Quadratic' and len(key_frames.items()) <= 2:
|
| 110 |
+
interp_method = 'Linear'
|
| 111 |
+
|
| 112 |
+
key_frame_series[0] = key_frame_series[key_frame_series.first_valid_index()]
|
| 113 |
+
key_frame_series[self.max_frames-1] = key_frame_series[key_frame_series.last_valid_index()]
|
| 114 |
+
key_frame_series = key_frame_series.interpolate(method=interp_method.lower(), limit_direction='both')
|
| 115 |
+
if integer:
|
| 116 |
+
return key_frame_series.astype(int)
|
| 117 |
+
return key_frame_series
|
| 118 |
+
|
| 119 |
+
def parse_key_frames(self, string):
|
| 120 |
+
# because math functions (i.e. sin(t)) can utilize brackets
|
| 121 |
+
# it extracts the value in form of some stuff
|
| 122 |
+
# which has previously been enclosed with brackets and
|
| 123 |
+
# with a comma or end of line existing after the closing one
|
| 124 |
+
frames = dict()
|
| 125 |
+
for match_object in string.split(","):
|
| 126 |
+
frameParam = match_object.split(":")
|
| 127 |
+
max_f = self.max_frames -1
|
| 128 |
+
s = self.seed
|
| 129 |
+
frame = int(self.sanitize_value(frameParam[0])) if check_is_number(self.sanitize_value(frameParam[0].strip())) else int(numexpr.evaluate(frameParam[0].strip().replace("'","",1).replace('"',"",1)[::-1].replace("'","",1).replace('"',"",1)[::-1]))
|
| 130 |
+
frames[frame] = frameParam[1].strip()
|
| 131 |
+
if frames == {} and len(string) != 0:
|
| 132 |
+
raise RuntimeError('Key Frame string not correctly formatted')
|
| 133 |
+
return frames
|
scripts/deforum_helpers/args.py
ADDED
|
@@ -0,0 +1,1134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import tempfile
|
| 4 |
+
import time
|
| 5 |
+
from types import SimpleNamespace
|
| 6 |
+
import modules.paths as ph
|
| 7 |
+
import modules.shared as sh
|
| 8 |
+
from modules.sd_samplers import samplers_for_img2img
|
| 9 |
+
from modules.processing import get_fixed_seed
|
| 10 |
+
from .defaults import get_guided_imgs_default_json, mask_fill_choices
|
| 11 |
+
from .deforum_controlnet import controlnet_component_names
|
| 12 |
+
from .general_utils import get_os, substitute_placeholders
|
| 13 |
+
|
| 14 |
+
def RootArgs():
|
| 15 |
+
return {
|
| 16 |
+
"device": sh.device,
|
| 17 |
+
"models_path": ph.models_path + '/Deforum',
|
| 18 |
+
"half_precision": not sh.cmd_opts.no_half,
|
| 19 |
+
"clipseg_model": None,
|
| 20 |
+
"mask_preset_names": ['everywhere', 'video_mask'],
|
| 21 |
+
"frames_cache": [],
|
| 22 |
+
"raw_batch_name": None,
|
| 23 |
+
"raw_seed": None,
|
| 24 |
+
"timestring": "",
|
| 25 |
+
"subseed": -1,
|
| 26 |
+
"subseed_strength": 0,
|
| 27 |
+
"seed_internal": 0,
|
| 28 |
+
"init_sample": None,
|
| 29 |
+
"noise_mask": None,
|
| 30 |
+
"initial_info": None,
|
| 31 |
+
"first_frame": None,
|
| 32 |
+
"animation_prompts": None,
|
| 33 |
+
"current_user_os": get_os(),
|
| 34 |
+
"tmp_deforum_run_duplicated_folder": os.path.join(tempfile.gettempdir(), 'tmp_run_deforum')
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# 'Midas-3.1-BeitLarge' is temporarily removed until fixed. Can add it back anytime as it's supported in the back-end depth code
|
| 38 |
+
def DeforumAnimArgs():
|
| 39 |
+
return {
|
| 40 |
+
"animation_mode": {
|
| 41 |
+
"label": "Animation mode",
|
| 42 |
+
"type": "radio",
|
| 43 |
+
"choices": ['2D', '3D', 'Video Input', 'Interpolation'],
|
| 44 |
+
"value": "2D",
|
| 45 |
+
"info": "control animation mode, will hide non relevant params upon change"
|
| 46 |
+
},
|
| 47 |
+
"max_frames": {
|
| 48 |
+
"label": "Max frames",
|
| 49 |
+
"type": "number",
|
| 50 |
+
"precision": 0,
|
| 51 |
+
"value": 120,
|
| 52 |
+
"info": "end the animation at this frame number",
|
| 53 |
+
},
|
| 54 |
+
"border": {
|
| 55 |
+
"label": "Border mode",
|
| 56 |
+
"type": "radio",
|
| 57 |
+
"choices": ['replicate', 'wrap'],
|
| 58 |
+
"value": "replicate",
|
| 59 |
+
"info": "controls pixel generation method for images smaller than the frame. hover on the options to see more info"
|
| 60 |
+
},
|
| 61 |
+
"angle": {
|
| 62 |
+
"label": "Angle",
|
| 63 |
+
"type": "textbox",
|
| 64 |
+
"value": "0: (0)",
|
| 65 |
+
"info": "rotate canvas clockwise/anticlockwise in degrees per frame"
|
| 66 |
+
},
|
| 67 |
+
|
| 68 |
+
"zoom": {
|
| 69 |
+
"label": "Zoom",
|
| 70 |
+
"type": "textbox",
|
| 71 |
+
"value": "0: (1.0025+0.002*sin(1.25*3.14*t/30))",
|
| 72 |
+
"info": "scale the canvas size, multiplicatively. [static = 1.0]"
|
| 73 |
+
},
|
| 74 |
+
|
| 75 |
+
"translation_x": {
|
| 76 |
+
"label": "Translation X",
|
| 77 |
+
"type": "textbox",
|
| 78 |
+
"value": "0: (0)",
|
| 79 |
+
"info": "move canvas left/right in pixels per frame"
|
| 80 |
+
},
|
| 81 |
+
|
| 82 |
+
"translation_y": {
|
| 83 |
+
"label": "Translation Y",
|
| 84 |
+
"type": "textbox",
|
| 85 |
+
"value": "0: (0)",
|
| 86 |
+
"info": "move canvas up/down in pixels per frame"
|
| 87 |
+
},
|
| 88 |
+
"translation_z": {
|
| 89 |
+
"label": "Translation Z",
|
| 90 |
+
"type": "textbox",
|
| 91 |
+
"value": "0: (1.75)",
|
| 92 |
+
"info": "move canvas towards/away from view [speed set by FOV]"
|
| 93 |
+
},
|
| 94 |
+
"transform_center_x": {
|
| 95 |
+
"label": "Transform Center X",
|
| 96 |
+
"type": "textbox",
|
| 97 |
+
"value": "0: (0.5)",
|
| 98 |
+
"info": "X center axis for 2D angle/zoom"
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
"transform_center_y": {
|
| 102 |
+
"label": "Transform Center Y",
|
| 103 |
+
"type": "textbox",
|
| 104 |
+
"value": "0: (0.5)",
|
| 105 |
+
"info": "Y center axis for 2D angle/zoom"
|
| 106 |
+
},
|
| 107 |
+
"rotation_3d_x": {
|
| 108 |
+
"label": "Rotation 3D X",
|
| 109 |
+
"type": "textbox",
|
| 110 |
+
"value": "0: (0)",
|
| 111 |
+
"info": "tilt canvas up/down in degrees per frame"
|
| 112 |
+
},
|
| 113 |
+
"rotation_3d_y": {
|
| 114 |
+
"label": "Rotation 3D Y",
|
| 115 |
+
"type": "textbox",
|
| 116 |
+
"value": "0: (0)",
|
| 117 |
+
"info": "pan canvas left/right in degrees per frame"
|
| 118 |
+
},
|
| 119 |
+
"rotation_3d_z": {
|
| 120 |
+
"label": "Rotation 3D Z",
|
| 121 |
+
"type": "textbox",
|
| 122 |
+
"value": "0: (0)",
|
| 123 |
+
"info": "roll canvas clockwise/anticlockwise"
|
| 124 |
+
},
|
| 125 |
+
"enable_perspective_flip": {
|
| 126 |
+
"label": "Enable perspective flip",
|
| 127 |
+
"type": "checkbox",
|
| 128 |
+
"value": False,
|
| 129 |
+
"info": ""
|
| 130 |
+
},
|
| 131 |
+
"perspective_flip_theta": {
|
| 132 |
+
"label": "Perspective flip theta",
|
| 133 |
+
"type": "textbox",
|
| 134 |
+
"value": "0: (0)",
|
| 135 |
+
"info": ""
|
| 136 |
+
},
|
| 137 |
+
"perspective_flip_phi": {
|
| 138 |
+
"label": "Perspective flip phi",
|
| 139 |
+
"type": "textbox",
|
| 140 |
+
"value": "0: (0)",
|
| 141 |
+
"info": ""
|
| 142 |
+
},
|
| 143 |
+
"perspective_flip_gamma": {
|
| 144 |
+
"label": "Perspective flip gamma",
|
| 145 |
+
"type": "textbox",
|
| 146 |
+
"value": "0: (0)",
|
| 147 |
+
"info": ""
|
| 148 |
+
},
|
| 149 |
+
"perspective_flip_fv": {
|
| 150 |
+
"label": "Perspective flip tv",
|
| 151 |
+
"type": "textbox",
|
| 152 |
+
"value": "0: (53)",
|
| 153 |
+
"info": "the 2D vanishing point of perspective (rec. range 30-160)"
|
| 154 |
+
},
|
| 155 |
+
"noise_schedule": {
|
| 156 |
+
"label": "Noise schedule",
|
| 157 |
+
"type": "textbox",
|
| 158 |
+
"value": "0: (0.065)",
|
| 159 |
+
"info": ""
|
| 160 |
+
},
|
| 161 |
+
"strength_schedule": {
|
| 162 |
+
"label": "Strength schedule",
|
| 163 |
+
"type": "textbox",
|
| 164 |
+
"value": "0: (0.65)",
|
| 165 |
+
"info": "amount of presence of previous frame to influence next frame, also controls steps in the following formula [steps - (strength_schedule * steps)]"
|
| 166 |
+
},
|
| 167 |
+
"contrast_schedule": "0: (1.0)",
|
| 168 |
+
"cfg_scale_schedule": {
|
| 169 |
+
"label": "CFG scale schedule",
|
| 170 |
+
"type": "textbox",
|
| 171 |
+
"value": "0: (7)",
|
| 172 |
+
"info": "how closely the image should conform to the prompt. Lower values produce more creative results. (recommended range 5-15)`"
|
| 173 |
+
},
|
| 174 |
+
"enable_steps_scheduling": {
|
| 175 |
+
"label": "Enable steps scheduling",
|
| 176 |
+
"type": "checkbox",
|
| 177 |
+
"value": False,
|
| 178 |
+
"info": ""
|
| 179 |
+
},
|
| 180 |
+
"steps_schedule": {
|
| 181 |
+
"label": "Steps schedule",
|
| 182 |
+
"type": "textbox",
|
| 183 |
+
"value": "0: (25)",
|
| 184 |
+
"info": "mainly allows using more than 200 steps. otherwise, it's a mirror-like param of 'strength schedule'"
|
| 185 |
+
},
|
| 186 |
+
"fov_schedule": {
|
| 187 |
+
"label": "FOV schedule",
|
| 188 |
+
"type": "textbox",
|
| 189 |
+
"value": "0: (70)",
|
| 190 |
+
"info": "adjusts the scale at which the canvas is moved in 3D by the translation_z value. [maximum range -180 to +180, with 0 being undefined. Values closer to 180 will make the image have less depth, while values closer to 0 will allow more depth]"
|
| 191 |
+
},
|
| 192 |
+
"aspect_ratio_schedule": {
|
| 193 |
+
"label": "Aspect Ratio schedule",
|
| 194 |
+
"type": "textbox",
|
| 195 |
+
"value": "0: (1)",
|
| 196 |
+
"info": "adjusts the aspect ratio for the depth calculations"
|
| 197 |
+
},
|
| 198 |
+
"aspect_ratio_use_old_formula": {
|
| 199 |
+
"label": "Use old aspect ratio formula",
|
| 200 |
+
"type": "checkbox",
|
| 201 |
+
"value": False,
|
| 202 |
+
"info": "for backward compatibility. uses the formula: `width/height`"
|
| 203 |
+
},
|
| 204 |
+
"near_schedule": {
|
| 205 |
+
"label": "Near schedule",
|
| 206 |
+
"type": "textbox",
|
| 207 |
+
"value": "0: (200)",
|
| 208 |
+
"info": ""
|
| 209 |
+
},
|
| 210 |
+
"far_schedule": {
|
| 211 |
+
"label": "Far schedule",
|
| 212 |
+
"type": "textbox",
|
| 213 |
+
"value": "0: (10000)",
|
| 214 |
+
"info": ""
|
| 215 |
+
},
|
| 216 |
+
"seed_schedule": {
|
| 217 |
+
"label": "Seed schedule",
|
| 218 |
+
"type": "textbox",
|
| 219 |
+
"value": '0:(s), 1:(-1), "max_f-2":(-1), "max_f-1":(s)',
|
| 220 |
+
"info": ""
|
| 221 |
+
},
|
| 222 |
+
"pix2pix_img_cfg_scale_schedule": {
|
| 223 |
+
"label": "Pix2Pix img CFG schedule",
|
| 224 |
+
"type": "textbox",
|
| 225 |
+
"value": "0:(1.5)",
|
| 226 |
+
"info": "ONLY in use when working with a P2P ckpt!"
|
| 227 |
+
},
|
| 228 |
+
"enable_subseed_scheduling": {
|
| 229 |
+
"label": "Enable Subseed scheduling",
|
| 230 |
+
"type": "checkbox",
|
| 231 |
+
"value": False,
|
| 232 |
+
"info": ""
|
| 233 |
+
},
|
| 234 |
+
"subseed_schedule": {
|
| 235 |
+
"label": "Subseed schedule",
|
| 236 |
+
"type": "textbox",
|
| 237 |
+
"value": "0: (1)",
|
| 238 |
+
"info": ""
|
| 239 |
+
},
|
| 240 |
+
"subseed_strength_schedule": {
|
| 241 |
+
"label": "Subseed strength schedule",
|
| 242 |
+
"type": "textbox",
|
| 243 |
+
"value": "0: (0)",
|
| 244 |
+
"info": ""
|
| 245 |
+
},
|
| 246 |
+
"enable_sampler_scheduling": {
|
| 247 |
+
"label": "Enable sampler scheduling",
|
| 248 |
+
"type": "checkbox",
|
| 249 |
+
"value": False,
|
| 250 |
+
"info": ""
|
| 251 |
+
},
|
| 252 |
+
"sampler_schedule": {
|
| 253 |
+
"label": "Sampler schedule",
|
| 254 |
+
"type": "textbox",
|
| 255 |
+
"value": '0: ("Euler a")',
|
| 256 |
+
"info": "allows keyframing different samplers. Use names as they appear in ui dropdown in 'run' tab"
|
| 257 |
+
},
|
| 258 |
+
"use_noise_mask": {
|
| 259 |
+
"label": "Use noise mask",
|
| 260 |
+
"type": "checkbox",
|
| 261 |
+
"value": False,
|
| 262 |
+
"info": ""
|
| 263 |
+
},
|
| 264 |
+
"mask_schedule": {
|
| 265 |
+
"label": "Mask schedule",
|
| 266 |
+
"type": "textbox",
|
| 267 |
+
"value": '0: ("{video_mask}")',
|
| 268 |
+
"info": ""
|
| 269 |
+
},
|
| 270 |
+
"noise_mask_schedule": {
|
| 271 |
+
"label": "Noise mask schedule",
|
| 272 |
+
"type": "textbox",
|
| 273 |
+
"value": '0: ("{video_mask}")',
|
| 274 |
+
"info": ""
|
| 275 |
+
},
|
| 276 |
+
"enable_checkpoint_scheduling": {
|
| 277 |
+
"label": "Enable checkpoint scheduling",
|
| 278 |
+
"type": "checkbox",
|
| 279 |
+
"value": False,
|
| 280 |
+
"info": ""
|
| 281 |
+
},
|
| 282 |
+
"checkpoint_schedule": {
|
| 283 |
+
"label": "allows keyframing different sd models. use *full* name as appears in ui dropdown",
|
| 284 |
+
"type": "textbox",
|
| 285 |
+
"value": '0: ("model1.ckpt"), 100: ("model2.safetensors")',
|
| 286 |
+
"info": "allows keyframing different sd models. use *full* name as appears in ui dropdown"
|
| 287 |
+
},
|
| 288 |
+
"enable_clipskip_scheduling": {
|
| 289 |
+
"label": "Enable CLIP skip scheduling",
|
| 290 |
+
"type": "checkbox",
|
| 291 |
+
"value": False,
|
| 292 |
+
"info": ""
|
| 293 |
+
},
|
| 294 |
+
"clipskip_schedule": {
|
| 295 |
+
"label": "CLIP skip schedule",
|
| 296 |
+
"type": "textbox",
|
| 297 |
+
"value": "0: (2)",
|
| 298 |
+
"info": ""
|
| 299 |
+
},
|
| 300 |
+
"enable_noise_multiplier_scheduling": {
|
| 301 |
+
"label": "Enable noise multiplier scheduling",
|
| 302 |
+
"type": "checkbox",
|
| 303 |
+
"value": True,
|
| 304 |
+
"info": ""
|
| 305 |
+
},
|
| 306 |
+
"noise_multiplier_schedule": {
|
| 307 |
+
"label": "Noise multiplier schedule",
|
| 308 |
+
"type": "textbox",
|
| 309 |
+
"value": "0: (1.05)",
|
| 310 |
+
"info": ""
|
| 311 |
+
},
|
| 312 |
+
"resume_from_timestring": {
|
| 313 |
+
"label": "Resume from timestring",
|
| 314 |
+
"type": "checkbox",
|
| 315 |
+
"value": False,
|
| 316 |
+
"info": ""
|
| 317 |
+
},
|
| 318 |
+
"resume_timestring": {
|
| 319 |
+
"label": "Resume timestring",
|
| 320 |
+
"type": "textbox",
|
| 321 |
+
"value": "20230129210106",
|
| 322 |
+
"info": ""
|
| 323 |
+
},
|
| 324 |
+
"enable_ddim_eta_scheduling": {
|
| 325 |
+
"label": "Enable DDIM ETA scheduling",
|
| 326 |
+
"type": "checkbox",
|
| 327 |
+
"value": False,
|
| 328 |
+
"visible": False,
|
| 329 |
+
"info": "noise multiplier; higher = more unpredictable results"
|
| 330 |
+
},
|
| 331 |
+
"ddim_eta_schedule": {
|
| 332 |
+
"label": "DDIM ETA Schedule",
|
| 333 |
+
"type": "textbox",
|
| 334 |
+
"value": "0: (0)",
|
| 335 |
+
"visible": False,
|
| 336 |
+
"info": ""
|
| 337 |
+
},
|
| 338 |
+
"enable_ancestral_eta_scheduling": {
|
| 339 |
+
"label": "Enable Ancestral ETA scheduling",
|
| 340 |
+
"type": "checkbox",
|
| 341 |
+
"value": False,
|
| 342 |
+
"info": "noise multiplier; applies to Euler a and other samplers that have the letter 'a' in them"
|
| 343 |
+
},
|
| 344 |
+
"ancestral_eta_schedule": {
|
| 345 |
+
"label": "Ancestral ETA Schedule",
|
| 346 |
+
"type": "textbox",
|
| 347 |
+
"value": "0: (1)",
|
| 348 |
+
"visible": False,
|
| 349 |
+
"info": ""
|
| 350 |
+
},
|
| 351 |
+
"amount_schedule": {
|
| 352 |
+
"label": "Amount schedule",
|
| 353 |
+
"type": "textbox",
|
| 354 |
+
"value": "0: (0.1)",
|
| 355 |
+
"info": ""
|
| 356 |
+
},
|
| 357 |
+
"kernel_schedule": {
|
| 358 |
+
"label": "Kernel schedule",
|
| 359 |
+
"type": "textbox",
|
| 360 |
+
"value": "0: (5)",
|
| 361 |
+
"info": ""
|
| 362 |
+
},
|
| 363 |
+
"sigma_schedule": {
|
| 364 |
+
"label": "Sigma schedule",
|
| 365 |
+
"type": "textbox",
|
| 366 |
+
"value": "0: (1)",
|
| 367 |
+
"info": ""
|
| 368 |
+
},
|
| 369 |
+
"threshold_schedule": {
|
| 370 |
+
"label": "Threshold schedule",
|
| 371 |
+
"type": "textbox",
|
| 372 |
+
"value": "0: (0)",
|
| 373 |
+
"info": ""
|
| 374 |
+
},
|
| 375 |
+
"color_coherence": {
|
| 376 |
+
"label": "Color coherence",
|
| 377 |
+
"type": "dropdown",
|
| 378 |
+
"choices": ['None', 'HSV', 'LAB', 'RGB', 'Video Input', 'Image'],
|
| 379 |
+
"value": "LAB",
|
| 380 |
+
"info": "choose an algorithm/ method for keeping color coherence across the animation"
|
| 381 |
+
},
|
| 382 |
+
"color_coherence_image_path": {
|
| 383 |
+
"label": "Color coherence image path",
|
| 384 |
+
"type": "textbox",
|
| 385 |
+
"value": "",
|
| 386 |
+
"info": ""
|
| 387 |
+
},
|
| 388 |
+
"color_coherence_video_every_N_frames": {
|
| 389 |
+
"label": "Color coherence video every N frames",
|
| 390 |
+
"type": "number",
|
| 391 |
+
"precision": 0,
|
| 392 |
+
"value": 1,
|
| 393 |
+
"info": "",
|
| 394 |
+
},
|
| 395 |
+
"color_force_grayscale": {
|
| 396 |
+
"label": "Color force Grayscale",
|
| 397 |
+
"type": "checkbox",
|
| 398 |
+
"value": False,
|
| 399 |
+
"info": "force all frames to be in grayscale"
|
| 400 |
+
},
|
| 401 |
+
"legacy_colormatch": {
|
| 402 |
+
"label": "Legacy colormatch",
|
| 403 |
+
"type": "checkbox",
|
| 404 |
+
"value": False,
|
| 405 |
+
"info": "apply colormatch before adding noise (use with CN's Tile)"
|
| 406 |
+
},
|
| 407 |
+
"diffusion_cadence": {
|
| 408 |
+
"label": "Cadence",
|
| 409 |
+
"type": "slider",
|
| 410 |
+
"minimum": 1,
|
| 411 |
+
"maximum": 50,
|
| 412 |
+
"step": 1,
|
| 413 |
+
"value": 2,
|
| 414 |
+
"info": "# of in-between frames that will not be directly diffused"
|
| 415 |
+
},
|
| 416 |
+
"optical_flow_cadence": {
|
| 417 |
+
"label": "Optical flow cadence",
|
| 418 |
+
"type": "dropdown",
|
| 419 |
+
"choices": ['None', 'RAFT', 'DIS Medium', 'DIS Fine', 'Farneback'],
|
| 420 |
+
"value": "None",
|
| 421 |
+
"info": "use optical flow estimation for your in-between (cadence) frames"
|
| 422 |
+
},
|
| 423 |
+
"cadence_flow_factor_schedule": {
|
| 424 |
+
"label": "Cadence flow factor schedule",
|
| 425 |
+
"type": "textbox",
|
| 426 |
+
"value": "0: (1)",
|
| 427 |
+
"info": ""
|
| 428 |
+
},
|
| 429 |
+
"optical_flow_redo_generation": {
|
| 430 |
+
"label": "Optical flow generation",
|
| 431 |
+
"type": "dropdown",
|
| 432 |
+
"choices": ['None', 'RAFT', 'DIS Medium', 'DIS Fine', 'Farneback'],
|
| 433 |
+
"value": "None",
|
| 434 |
+
"info": "this option takes twice as long because it generates twice in order to capture the optical flow from the previous image to the first generation, then warps the previous image and redoes the generation"
|
| 435 |
+
},
|
| 436 |
+
"redo_flow_factor_schedule": {
|
| 437 |
+
"label": "Generation flow factor schedule",
|
| 438 |
+
"type": "textbox",
|
| 439 |
+
"value": "0: (1)",
|
| 440 |
+
"info": ""
|
| 441 |
+
},
|
| 442 |
+
"diffusion_redo": '0',
|
| 443 |
+
"noise_type": {
|
| 444 |
+
"label": "Noise type",
|
| 445 |
+
"type": "radio",
|
| 446 |
+
"choices": ['uniform', 'perlin'],
|
| 447 |
+
"value": "perlin",
|
| 448 |
+
"info": ""
|
| 449 |
+
},
|
| 450 |
+
"perlin_w": {
|
| 451 |
+
"label": "Perlin W",
|
| 452 |
+
"type": "slider",
|
| 453 |
+
"minimum": 0.1,
|
| 454 |
+
"maximum": 16,
|
| 455 |
+
"step": 0.1,
|
| 456 |
+
"value": 8,
|
| 457 |
+
"visible": False
|
| 458 |
+
},
|
| 459 |
+
"perlin_h": {
|
| 460 |
+
"label": "Perlin H",
|
| 461 |
+
"type": "slider",
|
| 462 |
+
"minimum": 0.1,
|
| 463 |
+
"maximum": 16,
|
| 464 |
+
"step": 0.1,
|
| 465 |
+
"value": 8,
|
| 466 |
+
"visible": False
|
| 467 |
+
},
|
| 468 |
+
"perlin_octaves": {
|
| 469 |
+
"label": "Perlin octaves",
|
| 470 |
+
"type": "slider",
|
| 471 |
+
"minimum": 1,
|
| 472 |
+
"maximum": 7,
|
| 473 |
+
"step": 1,
|
| 474 |
+
"value": 4
|
| 475 |
+
},
|
| 476 |
+
"perlin_persistence": {
|
| 477 |
+
"label": "Perlin persistence",
|
| 478 |
+
"type": "slider",
|
| 479 |
+
"minimum": 0,
|
| 480 |
+
"maximum": 1,
|
| 481 |
+
"step": 0.02,
|
| 482 |
+
"value": 0.5
|
| 483 |
+
},
|
| 484 |
+
"use_depth_warping": {
|
| 485 |
+
"label": "Use depth warping",
|
| 486 |
+
"type": "checkbox",
|
| 487 |
+
"value": True,
|
| 488 |
+
"info": ""
|
| 489 |
+
},
|
| 490 |
+
"depth_algorithm": {
|
| 491 |
+
"label": "Depth Algorithm",
|
| 492 |
+
"type": "dropdown",
|
| 493 |
+
"choices": ['Midas+AdaBins (old)', 'Zoe+AdaBins (old)', 'Midas-3-Hybrid', 'AdaBins', 'Zoe', 'Leres'],
|
| 494 |
+
"value": "Midas-3-Hybrid",
|
| 495 |
+
"info": "choose an algorithm/ method for keeping color coherence across the animation"
|
| 496 |
+
},
|
| 497 |
+
"midas_weight": {
|
| 498 |
+
"label": "MiDaS/Zoe weight",
|
| 499 |
+
"type": "number",
|
| 500 |
+
"precision": None,
|
| 501 |
+
"value": 0.2,
|
| 502 |
+
"info": "sets a midpoint at which a depth-map is to be drawn: range [-1 to +1]",
|
| 503 |
+
"visible": False
|
| 504 |
+
},
|
| 505 |
+
"padding_mode": {
|
| 506 |
+
"label": "Padding mode",
|
| 507 |
+
"type": "radio",
|
| 508 |
+
"choices": ['border', 'reflection', 'zeros'],
|
| 509 |
+
"value": "border",
|
| 510 |
+
"info": "controls the handling of pixels outside the field of view as they come into the scene"
|
| 511 |
+
},
|
| 512 |
+
"sampling_mode": {
|
| 513 |
+
"label": "Padding mode",
|
| 514 |
+
"type": "radio",
|
| 515 |
+
"choices": ['bicubic', 'bilinear', 'nearest'],
|
| 516 |
+
"value": "bicubic",
|
| 517 |
+
"info": ""
|
| 518 |
+
},
|
| 519 |
+
"save_depth_maps": {
|
| 520 |
+
"label": "Save 3D depth maps",
|
| 521 |
+
"type": "checkbox",
|
| 522 |
+
"value": False,
|
| 523 |
+
"info": "save animation's depth maps as extra files"
|
| 524 |
+
},
|
| 525 |
+
"video_init_path": {
|
| 526 |
+
"label": "Video init path/ URL",
|
| 527 |
+
"type": "textbox",
|
| 528 |
+
"value": 'https://deforum.github.io/a1/V1.mp4',
|
| 529 |
+
"info": ""
|
| 530 |
+
},
|
| 531 |
+
"extract_nth_frame": {
|
| 532 |
+
"label": "Extract nth frame",
|
| 533 |
+
"type": "number",
|
| 534 |
+
"precision": 0,
|
| 535 |
+
"value": 1,
|
| 536 |
+
"info": ""
|
| 537 |
+
},
|
| 538 |
+
"extract_from_frame": {
|
| 539 |
+
"label": "Extract from frame",
|
| 540 |
+
"type": "number",
|
| 541 |
+
"precision": 0,
|
| 542 |
+
"value": 0,
|
| 543 |
+
"info": ""
|
| 544 |
+
},
|
| 545 |
+
"extract_to_frame": {
|
| 546 |
+
"label": "Extract to frame",
|
| 547 |
+
"type": "number",
|
| 548 |
+
"precision": 0,
|
| 549 |
+
"value": -1,
|
| 550 |
+
"info": ""
|
| 551 |
+
},
|
| 552 |
+
"overwrite_extracted_frames": {
|
| 553 |
+
"label": "Overwrite extracted frames",
|
| 554 |
+
"type": "checkbox",
|
| 555 |
+
"value": False,
|
| 556 |
+
"info": ""
|
| 557 |
+
},
|
| 558 |
+
"use_mask_video": {
|
| 559 |
+
"label": "Use mask video",
|
| 560 |
+
"type": "checkbox",
|
| 561 |
+
"value": False,
|
| 562 |
+
"info": ""
|
| 563 |
+
},
|
| 564 |
+
"video_mask_path": {
|
| 565 |
+
"label": "Video mask path",
|
| 566 |
+
"type": "textbox",
|
| 567 |
+
"value": 'https://deforum.github.io/a1/VM1.mp4',
|
| 568 |
+
"info": ""
|
| 569 |
+
},
|
| 570 |
+
"hybrid_comp_alpha_schedule": {
|
| 571 |
+
"label": "Comp alpha schedule",
|
| 572 |
+
"type": "textbox",
|
| 573 |
+
"value": "0:(0.5)",
|
| 574 |
+
"info": ""
|
| 575 |
+
},
|
| 576 |
+
"hybrid_comp_mask_blend_alpha_schedule": {
|
| 577 |
+
"label": "Comp mask blend alpha schedule",
|
| 578 |
+
"type": "textbox",
|
| 579 |
+
"value": "0:(0.5)",
|
| 580 |
+
"info": ""
|
| 581 |
+
},
|
| 582 |
+
"hybrid_comp_mask_contrast_schedule": {
|
| 583 |
+
"label": "Comp mask contrast schedule",
|
| 584 |
+
"type": "textbox",
|
| 585 |
+
"value": "0:(1)",
|
| 586 |
+
"info": ""
|
| 587 |
+
},
|
| 588 |
+
"hybrid_comp_mask_auto_contrast_cutoff_high_schedule": {
|
| 589 |
+
"label": "Comp mask auto contrast cutoff high schedule",
|
| 590 |
+
"type": "textbox",
|
| 591 |
+
"value": "0:(100)",
|
| 592 |
+
"info": ""
|
| 593 |
+
},
|
| 594 |
+
"hybrid_comp_mask_auto_contrast_cutoff_low_schedule": {
|
| 595 |
+
"label": "Comp mask auto contrast cutoff low schedule",
|
| 596 |
+
"type": "textbox",
|
| 597 |
+
"value": "0:(0)",
|
| 598 |
+
"info": ""
|
| 599 |
+
},
|
| 600 |
+
"hybrid_flow_factor_schedule": {
|
| 601 |
+
"label": "Flow factor schedule",
|
| 602 |
+
"type": "textbox",
|
| 603 |
+
"value": "0:(1)",
|
| 604 |
+
"info": ""
|
| 605 |
+
},
|
| 606 |
+
"hybrid_generate_inputframes": {
|
| 607 |
+
"label": "Generate inputframes",
|
| 608 |
+
"type": "checkbox",
|
| 609 |
+
"value": False,
|
| 610 |
+
"info": ""
|
| 611 |
+
},
|
| 612 |
+
"hybrid_generate_human_masks": {
|
| 613 |
+
"label": "Generate human masks",
|
| 614 |
+
"type": "radio",
|
| 615 |
+
"choices": ['None', 'PNGs', 'Video', 'Both'],
|
| 616 |
+
"value": "None",
|
| 617 |
+
"info": ""
|
| 618 |
+
},
|
| 619 |
+
"hybrid_use_first_frame_as_init_image": {
|
| 620 |
+
"label": "First frame as init image",
|
| 621 |
+
"type": "checkbox",
|
| 622 |
+
"value": True,
|
| 623 |
+
"info": "",
|
| 624 |
+
"visible": False
|
| 625 |
+
},
|
| 626 |
+
"hybrid_motion": {
|
| 627 |
+
"label": "Hybrid motion",
|
| 628 |
+
"type": "radio",
|
| 629 |
+
"choices": ['None', 'Optical Flow', 'Perspective', 'Affine'],
|
| 630 |
+
"value": "None",
|
| 631 |
+
"info": ""
|
| 632 |
+
},
|
| 633 |
+
"hybrid_motion_use_prev_img": {
|
| 634 |
+
"label": "Motion use prev img",
|
| 635 |
+
"type": "checkbox",
|
| 636 |
+
"value": False,
|
| 637 |
+
"info": "",
|
| 638 |
+
"visible": False
|
| 639 |
+
},
|
| 640 |
+
"hybrid_flow_consistency": {
|
| 641 |
+
"label": "Flow consistency mask",
|
| 642 |
+
"type": "checkbox",
|
| 643 |
+
"value": False,
|
| 644 |
+
"info": "",
|
| 645 |
+
"visible": False
|
| 646 |
+
},
|
| 647 |
+
"hybrid_consistency_blur": {
|
| 648 |
+
"label": "Consistency mask blur",
|
| 649 |
+
"type": "slider",
|
| 650 |
+
"minimum": 0,
|
| 651 |
+
"maximum": 16,
|
| 652 |
+
"step": 1,
|
| 653 |
+
"value": 2,
|
| 654 |
+
"visible": False
|
| 655 |
+
},
|
| 656 |
+
"hybrid_flow_method": {
|
| 657 |
+
"label": "Flow method",
|
| 658 |
+
"type": "radio",
|
| 659 |
+
"choices": ['RAFT', 'DIS Medium', 'DIS Fine', 'Farneback'],
|
| 660 |
+
"value": "RAFT",
|
| 661 |
+
"info": "",
|
| 662 |
+
"visible": False
|
| 663 |
+
},
|
| 664 |
+
"hybrid_composite": 'None', # ['None', 'Normal', 'Before Motion', 'After Generation']
|
| 665 |
+
"hybrid_use_init_image": {
|
| 666 |
+
"label": "Use init image as video",
|
| 667 |
+
"type": "checkbox",
|
| 668 |
+
"value": False,
|
| 669 |
+
"info": "",
|
| 670 |
+
},
|
| 671 |
+
"hybrid_comp_mask_type": {
|
| 672 |
+
"label": "Comp mask type",
|
| 673 |
+
"type": "radio",
|
| 674 |
+
"choices": ['None', 'Depth', 'Video Depth', 'Blend', 'Difference'],
|
| 675 |
+
"value": "None",
|
| 676 |
+
"info": "",
|
| 677 |
+
"visible": False
|
| 678 |
+
},
|
| 679 |
+
"hybrid_comp_mask_inverse": False,
|
| 680 |
+
"hybrid_comp_mask_equalize": {
|
| 681 |
+
"label": "Comp mask equalize",
|
| 682 |
+
"type": "radio",
|
| 683 |
+
"choices": ['None', 'Before', 'After', 'Both'],
|
| 684 |
+
"value": "None",
|
| 685 |
+
"info": "",
|
| 686 |
+
},
|
| 687 |
+
"hybrid_comp_mask_auto_contrast": False,
|
| 688 |
+
"hybrid_comp_save_extra_frames": False
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
def DeforumArgs():
|
| 692 |
+
return {
|
| 693 |
+
"W": {
|
| 694 |
+
"label": "Width",
|
| 695 |
+
"type": "slider",
|
| 696 |
+
"minimum": 64,
|
| 697 |
+
"maximum": 2048,
|
| 698 |
+
"step": 64,
|
| 699 |
+
"value": 512,
|
| 700 |
+
},
|
| 701 |
+
"H": {
|
| 702 |
+
"label": "Height",
|
| 703 |
+
"type": "slider",
|
| 704 |
+
"minimum": 64,
|
| 705 |
+
"maximum": 2048,
|
| 706 |
+
"step": 64,
|
| 707 |
+
"value": 512,
|
| 708 |
+
},
|
| 709 |
+
"show_info_on_ui": True,
|
| 710 |
+
"tiling": {
|
| 711 |
+
"label": "Tiling",
|
| 712 |
+
"type": "checkbox",
|
| 713 |
+
"value": False,
|
| 714 |
+
"info": "enable for seamless-tiling of each generated image. Experimental"
|
| 715 |
+
},
|
| 716 |
+
"restore_faces": {
|
| 717 |
+
"label": "Restore faces",
|
| 718 |
+
"type": "checkbox",
|
| 719 |
+
"value": False,
|
| 720 |
+
"info": "enable to trigger webui's face restoration on each frame during the generation"
|
| 721 |
+
},
|
| 722 |
+
"seed_resize_from_w": {
|
| 723 |
+
"label": "Resize seed from width",
|
| 724 |
+
"type": "slider",
|
| 725 |
+
"minimum": 0,
|
| 726 |
+
"maximum": 2048,
|
| 727 |
+
"step": 64,
|
| 728 |
+
"value": 0,
|
| 729 |
+
},
|
| 730 |
+
"seed_resize_from_h": {
|
| 731 |
+
"label": "Resize seed from height",
|
| 732 |
+
"type": "slider",
|
| 733 |
+
"minimum": 0,
|
| 734 |
+
"maximum": 2048,
|
| 735 |
+
"step": 64,
|
| 736 |
+
"value": 0,
|
| 737 |
+
},
|
| 738 |
+
"seed": {
|
| 739 |
+
"label": "Seed",
|
| 740 |
+
"type": "number",
|
| 741 |
+
"precision": 0,
|
| 742 |
+
"value": -1,
|
| 743 |
+
"info": "Starting seed for the animation. -1 for random"
|
| 744 |
+
},
|
| 745 |
+
"sampler": {
|
| 746 |
+
"label": "Sampler",
|
| 747 |
+
"type": "dropdown",
|
| 748 |
+
"choices": [x.name for x in samplers_for_img2img],
|
| 749 |
+
"value": samplers_for_img2img[0].name,
|
| 750 |
+
},
|
| 751 |
+
"steps": {
|
| 752 |
+
"label": "step",
|
| 753 |
+
"type": "slider",
|
| 754 |
+
"minimum": 1,
|
| 755 |
+
"maximum": 200,
|
| 756 |
+
"step": 1,
|
| 757 |
+
"value": 25,
|
| 758 |
+
},
|
| 759 |
+
"batch_name": {
|
| 760 |
+
"label": "Batch name",
|
| 761 |
+
"type": "textbox",
|
| 762 |
+
"value": "Deforum_{timestring}",
|
| 763 |
+
"info": "output images will be placed in a folder with this name ({timestring} token will be replaced) inside the img2img output folder. Supports params placeholders. e.g {seed}, {w}, {h}, {prompts}"
|
| 764 |
+
},
|
| 765 |
+
"seed_behavior": {
|
| 766 |
+
"label": "Seed behavior",
|
| 767 |
+
"type": "radio",
|
| 768 |
+
"choices": ['iter', 'fixed', 'random', 'ladder', 'alternate', 'schedule'],
|
| 769 |
+
"value": "iter",
|
| 770 |
+
"info": "controls the seed behavior that is used for animation. hover on the options to see more info"
|
| 771 |
+
},
|
| 772 |
+
"seed_iter_N": {
|
| 773 |
+
"label": "Seed iter N",
|
| 774 |
+
"type": "number",
|
| 775 |
+
"precision": 0,
|
| 776 |
+
"value": 1,
|
| 777 |
+
"info": "for how many frames the same seed should stick before iterating to the next one"
|
| 778 |
+
},
|
| 779 |
+
"use_init": {
|
| 780 |
+
"label": "Use init",
|
| 781 |
+
"type": "checkbox",
|
| 782 |
+
"value": False,
|
| 783 |
+
"info": ""
|
| 784 |
+
},
|
| 785 |
+
"strength": {
|
| 786 |
+
"label": "strength",
|
| 787 |
+
"type": "slider",
|
| 788 |
+
"minimum": 0,
|
| 789 |
+
"maximum": 1,
|
| 790 |
+
"step": 0.01,
|
| 791 |
+
"value": 0.8,
|
| 792 |
+
},
|
| 793 |
+
"strength_0_no_init": {
|
| 794 |
+
"label": "Strength 0 no init",
|
| 795 |
+
"type": "checkbox",
|
| 796 |
+
"value": True,
|
| 797 |
+
"info": ""
|
| 798 |
+
},
|
| 799 |
+
"init_image": {
|
| 800 |
+
"label": "Init image",
|
| 801 |
+
"type": "textbox",
|
| 802 |
+
"value": "https://deforum.github.io/a1/I1.png",
|
| 803 |
+
"info": ""
|
| 804 |
+
},
|
| 805 |
+
"use_mask": {
|
| 806 |
+
"label": "Use mask",
|
| 807 |
+
"type": "checkbox",
|
| 808 |
+
"value": False,
|
| 809 |
+
"info": ""
|
| 810 |
+
},
|
| 811 |
+
"use_alpha_as_mask": {
|
| 812 |
+
"label": "Use alpha as mask",
|
| 813 |
+
"type": "checkbox",
|
| 814 |
+
"value": False,
|
| 815 |
+
"info": ""
|
| 816 |
+
},
|
| 817 |
+
"mask_file": {
|
| 818 |
+
"label": "Mask file",
|
| 819 |
+
"type": "textbox",
|
| 820 |
+
"value": "https://deforum.github.io/a1/M1.jpg",
|
| 821 |
+
"info": ""
|
| 822 |
+
},
|
| 823 |
+
"invert_mask": {
|
| 824 |
+
"label": "Invert mask",
|
| 825 |
+
"type": "checkbox",
|
| 826 |
+
"value": False,
|
| 827 |
+
"info": ""
|
| 828 |
+
},
|
| 829 |
+
"mask_contrast_adjust": {
|
| 830 |
+
"label": "Mask contrast adjust",
|
| 831 |
+
"type": "number",
|
| 832 |
+
"precision": None,
|
| 833 |
+
"value": 1.0,
|
| 834 |
+
"info": ""
|
| 835 |
+
},
|
| 836 |
+
"mask_brightness_adjust": {
|
| 837 |
+
"label": "Mask brightness adjust",
|
| 838 |
+
"type": "number",
|
| 839 |
+
"precision": None,
|
| 840 |
+
"value": 1.0,
|
| 841 |
+
"info": ""
|
| 842 |
+
},
|
| 843 |
+
"overlay_mask": {
|
| 844 |
+
"label": "Overlay mask",
|
| 845 |
+
"type": "checkbox",
|
| 846 |
+
"value": True,
|
| 847 |
+
"info": ""
|
| 848 |
+
},
|
| 849 |
+
"mask_overlay_blur": {
|
| 850 |
+
"label": "Mask overlay blur",
|
| 851 |
+
"type": "slider",
|
| 852 |
+
"minimum": 0,
|
| 853 |
+
"maximum": 64,
|
| 854 |
+
"step": 1,
|
| 855 |
+
"value": 4,
|
| 856 |
+
},
|
| 857 |
+
"fill": {
|
| 858 |
+
"label": "Mask fill",
|
| 859 |
+
"type": "radio",
|
| 860 |
+
"radio_type": "index",
|
| 861 |
+
"choices": ['fill', 'original', 'latent noise', 'latent nothing'],
|
| 862 |
+
"value": 'original',
|
| 863 |
+
"info": ""
|
| 864 |
+
},
|
| 865 |
+
"full_res_mask": {
|
| 866 |
+
"label": "Full res mask",
|
| 867 |
+
"type": "checkbox",
|
| 868 |
+
"value": True,
|
| 869 |
+
"info": ""
|
| 870 |
+
},
|
| 871 |
+
"full_res_mask_padding": {
|
| 872 |
+
"label": "Full res mask padding",
|
| 873 |
+
"type": "slider",
|
| 874 |
+
"minimum": 0,
|
| 875 |
+
"maximum": 512,
|
| 876 |
+
"step": 1,
|
| 877 |
+
"value": 4,
|
| 878 |
+
},
|
| 879 |
+
"reroll_blank_frames": {
|
| 880 |
+
"label": "Reroll blank frames",
|
| 881 |
+
"type": "radio",
|
| 882 |
+
"choices": ['reroll', 'interrupt', 'ignore'],
|
| 883 |
+
"value": "ignore",
|
| 884 |
+
"info": ""
|
| 885 |
+
},
|
| 886 |
+
"reroll_patience": {
|
| 887 |
+
"label": "Reroll patience",
|
| 888 |
+
"type": "number",
|
| 889 |
+
"precision": None,
|
| 890 |
+
"value": 10,
|
| 891 |
+
"info": ""
|
| 892 |
+
},
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
def LoopArgs():
|
| 896 |
+
return {
|
| 897 |
+
"use_looper": {
|
| 898 |
+
"label": "Enable guided images mode",
|
| 899 |
+
"type": "checkbox",
|
| 900 |
+
"value": False,
|
| 901 |
+
},
|
| 902 |
+
"init_images": {
|
| 903 |
+
"label": "Images to use for keyframe guidance",
|
| 904 |
+
"type": "textbox",
|
| 905 |
+
"lines": 9,
|
| 906 |
+
"value": get_guided_imgs_default_json(),
|
| 907 |
+
},
|
| 908 |
+
"image_strength_schedule": {
|
| 909 |
+
"label": "Image strength schedule",
|
| 910 |
+
"type": "textbox",
|
| 911 |
+
"value": "0:(0.75)",
|
| 912 |
+
},
|
| 913 |
+
"blendFactorMax": {
|
| 914 |
+
"label": "Blend factor max",
|
| 915 |
+
"type": "textbox",
|
| 916 |
+
"value": "0:(0.35)",
|
| 917 |
+
},
|
| 918 |
+
"blendFactorSlope": {
|
| 919 |
+
"label": "Blend factor slope",
|
| 920 |
+
"type": "textbox",
|
| 921 |
+
"value": "0:(0.25)",
|
| 922 |
+
},
|
| 923 |
+
"tweening_frames_schedule": {
|
| 924 |
+
"label": "Tweening frames schedule",
|
| 925 |
+
"type": "textbox",
|
| 926 |
+
"value": "0:(20)",
|
| 927 |
+
},
|
| 928 |
+
"color_correction_factor": {
|
| 929 |
+
"label": "Color correction factor",
|
| 930 |
+
"type": "textbox",
|
| 931 |
+
"value": "0:(0.075)",
|
| 932 |
+
}
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
def ParseqArgs():
|
| 936 |
+
return {
|
| 937 |
+
"parseq_manifest": {
|
| 938 |
+
"label": "Parseq Manifest (JSON or URL)",
|
| 939 |
+
"type": "textbox",
|
| 940 |
+
"lines": 4,
|
| 941 |
+
"value": None,
|
| 942 |
+
},
|
| 943 |
+
"parseq_use_deltas": {
|
| 944 |
+
"label": "Use delta values for movement parameters",
|
| 945 |
+
"type": "checkbox",
|
| 946 |
+
"value": True,
|
| 947 |
+
}
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
def DeforumOutputArgs():
|
| 951 |
+
return {
|
| 952 |
+
"skip_video_creation": {
|
| 953 |
+
"label": "Skip video creation",
|
| 954 |
+
"type": "checkbox",
|
| 955 |
+
"value": False,
|
| 956 |
+
"info": "If enabled, only images will be saved"
|
| 957 |
+
},
|
| 958 |
+
"fps": {
|
| 959 |
+
"label": "FPS",
|
| 960 |
+
"type": "slider",
|
| 961 |
+
"minimum": 1,
|
| 962 |
+
"maximum": 240,
|
| 963 |
+
"step": 1,
|
| 964 |
+
"value": 15,
|
| 965 |
+
},
|
| 966 |
+
"make_gif": {
|
| 967 |
+
"label": "Make GIF",
|
| 968 |
+
"type": "checkbox",
|
| 969 |
+
"value": False,
|
| 970 |
+
"info": "make gif in addition to the video/s"
|
| 971 |
+
},
|
| 972 |
+
"delete_imgs": {
|
| 973 |
+
"label": "Delete Imgs",
|
| 974 |
+
"type": "checkbox",
|
| 975 |
+
"value": False,
|
| 976 |
+
"info": "auto-delete imgs when video is ready"
|
| 977 |
+
},
|
| 978 |
+
"image_path": {
|
| 979 |
+
"label": "Image path",
|
| 980 |
+
"type": "textbox",
|
| 981 |
+
"value": "C:/SD/20230124234916_%09d.png",
|
| 982 |
+
},
|
| 983 |
+
"add_soundtrack": {
|
| 984 |
+
"label": "Add soundtrack",
|
| 985 |
+
"type": "radio",
|
| 986 |
+
"choices": ['None', 'File', 'Init Video'],
|
| 987 |
+
"value": "None",
|
| 988 |
+
"info": "add audio to video from file/url or init video"
|
| 989 |
+
},
|
| 990 |
+
"soundtrack_path": {
|
| 991 |
+
"label": "Soundtrack path",
|
| 992 |
+
"type": "textbox",
|
| 993 |
+
"value": "https://deforum.github.io/a1/A1.mp3",
|
| 994 |
+
"info": "abs. path or url to audio file"
|
| 995 |
+
},
|
| 996 |
+
"r_upscale_video": {
|
| 997 |
+
"label": "Upscale",
|
| 998 |
+
"type": "checkbox",
|
| 999 |
+
"value": False,
|
| 1000 |
+
"info": "upscale output imgs when run is finished"
|
| 1001 |
+
},
|
| 1002 |
+
"r_upscale_factor": {
|
| 1003 |
+
"label": "Upscale factor",
|
| 1004 |
+
"type": "dropdown",
|
| 1005 |
+
"choices": ['x2', 'x3', 'x4'],
|
| 1006 |
+
"value": "x2",
|
| 1007 |
+
},
|
| 1008 |
+
"r_upscale_model": {
|
| 1009 |
+
"label": "Upscale model",
|
| 1010 |
+
"type": "dropdown",
|
| 1011 |
+
"choices": ['realesr-animevideov3', 'realesrgan-x4plus', 'realesrgan-x4plus-anime'],
|
| 1012 |
+
"value": 'realesr-animevideov3',
|
| 1013 |
+
},
|
| 1014 |
+
"r_upscale_keep_imgs": {
|
| 1015 |
+
"label": "Keep Imgs",
|
| 1016 |
+
"type": "checkbox",
|
| 1017 |
+
"value": True,
|
| 1018 |
+
"info": "don't delete upscaled imgs",
|
| 1019 |
+
},
|
| 1020 |
+
"store_frames_in_ram": {
|
| 1021 |
+
"label": "Store frames in ram",
|
| 1022 |
+
"type": "checkbox",
|
| 1023 |
+
"value": False,
|
| 1024 |
+
"info": "auto-delete imgs when video is ready",
|
| 1025 |
+
"visible": False
|
| 1026 |
+
},
|
| 1027 |
+
"frame_interpolation_engine": {
|
| 1028 |
+
"label": "Engine",
|
| 1029 |
+
"type": "radio",
|
| 1030 |
+
"choices": ['None', 'RIFE v4.6', 'FILM'],
|
| 1031 |
+
"value": "None",
|
| 1032 |
+
"info": "select the frame interpolation engine. hover on the options for more info"
|
| 1033 |
+
},
|
| 1034 |
+
"frame_interpolation_x_amount": {
|
| 1035 |
+
"label": "Interp X",
|
| 1036 |
+
"type": "slider",
|
| 1037 |
+
"minimum": 2,
|
| 1038 |
+
"maximum": 10,
|
| 1039 |
+
"step": 1,
|
| 1040 |
+
"value": 2,
|
| 1041 |
+
},
|
| 1042 |
+
"frame_interpolation_slow_mo_enabled": {
|
| 1043 |
+
"label": "Slow Mo",
|
| 1044 |
+
"type": "checkbox",
|
| 1045 |
+
"value": False,
|
| 1046 |
+
"visible": False,
|
| 1047 |
+
"info": "Slow-Mo the interpolated video, audio will not be used if enabled",
|
| 1048 |
+
},
|
| 1049 |
+
"frame_interpolation_slow_mo_amount": {
|
| 1050 |
+
"label": "Slow-Mo X",
|
| 1051 |
+
"type": "slider",
|
| 1052 |
+
"minimum": 2,
|
| 1053 |
+
"maximum": 10,
|
| 1054 |
+
"step": 1,
|
| 1055 |
+
"value": 2,
|
| 1056 |
+
},
|
| 1057 |
+
"frame_interpolation_keep_imgs": {
|
| 1058 |
+
"label": "Keep Imgs",
|
| 1059 |
+
"type": "checkbox",
|
| 1060 |
+
"value": False,
|
| 1061 |
+
"info": "Keep interpolated images on disk",
|
| 1062 |
+
"visible": False
|
| 1063 |
+
},
|
| 1064 |
+
"frame_interpolation_use_upscaled": {
|
| 1065 |
+
"label": "Use Upscaled",
|
| 1066 |
+
"type": "checkbox",
|
| 1067 |
+
"value": False,
|
| 1068 |
+
"info": "Interpolate upscaled images, if available",
|
| 1069 |
+
"visible": False
|
| 1070 |
+
},
|
| 1071 |
+
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
def get_component_names():
|
| 1075 |
+
return ['override_settings_with_file', 'custom_settings_file', *DeforumAnimArgs().keys(), 'animation_prompts', 'animation_prompts_positive', 'animation_prompts_negative',
|
| 1076 |
+
*DeforumArgs().keys(), *DeforumOutputArgs().keys(), *ParseqArgs().keys(), *LoopArgs().keys(), *controlnet_component_names()]
|
| 1077 |
+
|
| 1078 |
+
def get_settings_component_names():
|
| 1079 |
+
return [name for name in get_component_names()]
|
| 1080 |
+
|
| 1081 |
+
def pack_args(args_dict, keys_function):
|
| 1082 |
+
return {name: args_dict[name] for name in keys_function()}
|
| 1083 |
+
|
| 1084 |
+
def process_args(args_dict_main, run_id):
|
| 1085 |
+
from .settings import load_args
|
| 1086 |
+
override_settings_with_file = args_dict_main['override_settings_with_file']
|
| 1087 |
+
custom_settings_file = args_dict_main['custom_settings_file']
|
| 1088 |
+
p = args_dict_main['p']
|
| 1089 |
+
|
| 1090 |
+
root = SimpleNamespace(**RootArgs())
|
| 1091 |
+
args = SimpleNamespace(**{name: args_dict_main[name] for name in DeforumArgs()})
|
| 1092 |
+
anim_args = SimpleNamespace(**{name: args_dict_main[name] for name in DeforumAnimArgs()})
|
| 1093 |
+
video_args = SimpleNamespace(**{name: args_dict_main[name] for name in DeforumOutputArgs()})
|
| 1094 |
+
parseq_args = SimpleNamespace(**{name: args_dict_main[name] for name in ParseqArgs()})
|
| 1095 |
+
loop_args = SimpleNamespace(**{name: args_dict_main[name] for name in LoopArgs()})
|
| 1096 |
+
controlnet_args = SimpleNamespace(**{name: args_dict_main[name] for name in controlnet_component_names()})
|
| 1097 |
+
|
| 1098 |
+
root.animation_prompts = json.loads(args_dict_main['animation_prompts'])
|
| 1099 |
+
|
| 1100 |
+
args_loaded_ok = True
|
| 1101 |
+
if override_settings_with_file:
|
| 1102 |
+
args_loaded_ok = load_args(args_dict_main, args, anim_args, parseq_args, loop_args, controlnet_args, video_args, custom_settings_file, root, run_id)
|
| 1103 |
+
|
| 1104 |
+
positive_prompts = args_dict_main['animation_prompts_positive']
|
| 1105 |
+
negative_prompts = args_dict_main['animation_prompts_negative']
|
| 1106 |
+
negative_prompts = negative_prompts.replace('--neg', '') # remove --neg from negative_prompts if received by mistake
|
| 1107 |
+
root.animation_prompts = {key: f"{positive_prompts} {val} {'' if '--neg' in val else '--neg'} {negative_prompts}" for key, val in root.animation_prompts.items()}
|
| 1108 |
+
|
| 1109 |
+
if args.seed == -1:
|
| 1110 |
+
root.raw_seed = -1
|
| 1111 |
+
args.seed = get_fixed_seed(args.seed)
|
| 1112 |
+
if root.raw_seed != -1:
|
| 1113 |
+
root.raw_seed = args.seed
|
| 1114 |
+
root.timestring = time.strftime('%Y%m%d%H%M%S')
|
| 1115 |
+
args.strength = max(0.0, min(1.0, args.strength))
|
| 1116 |
+
args.prompts = json.loads(args_dict_main['animation_prompts'])
|
| 1117 |
+
args.positive_prompts = args_dict_main['animation_prompts_positive']
|
| 1118 |
+
args.negative_prompts = args_dict_main['animation_prompts_negative']
|
| 1119 |
+
|
| 1120 |
+
if not args.use_init and not anim_args.hybrid_use_init_image:
|
| 1121 |
+
args.init_image = None
|
| 1122 |
+
|
| 1123 |
+
elif anim_args.animation_mode == 'Video Input':
|
| 1124 |
+
args.use_init = True
|
| 1125 |
+
|
| 1126 |
+
current_arg_list = [args, anim_args, video_args, parseq_args, root]
|
| 1127 |
+
full_base_folder_path = os.path.join(os.getcwd(), p.outpath_samples)
|
| 1128 |
+
root.raw_batch_name = args.batch_name
|
| 1129 |
+
args.batch_name = substitute_placeholders(args.batch_name, current_arg_list, full_base_folder_path)
|
| 1130 |
+
args.outdir = os.path.join(p.outpath_samples, str(args.batch_name))
|
| 1131 |
+
args.outdir = os.path.join(os.getcwd(), args.outdir)
|
| 1132 |
+
os.makedirs(args.outdir, exist_ok=True)
|
| 1133 |
+
|
| 1134 |
+
return args_loaded_ok, root, args, anim_args, video_args, parseq_args, loop_args, controlnet_args
|
scripts/deforum_helpers/auto_navigation.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
# reallybigname - auto-navigation functions in progress...
|
| 5 |
+
# usage:
|
| 6 |
+
# if auto_rotation:
|
| 7 |
+
# rot_mat = rotate_camera_towards_depth(depth_tensor, auto_rotation_steps, w, h, fov_deg, auto_rotation_depth_target)
|
| 8 |
+
def rotate_camera_towards_depth(depth_tensor, turn_weight, width, height, h_fov=60, target_depth=1):
|
| 9 |
+
# Compute the depth at the target depth
|
| 10 |
+
target_depth_index = int(target_depth * depth_tensor.shape[0])
|
| 11 |
+
target_depth_values = depth_tensor[target_depth_index]
|
| 12 |
+
max_depth_index = torch.argmax(target_depth_values).item()
|
| 13 |
+
max_depth_index = (max_depth_index, target_depth_index)
|
| 14 |
+
max_depth = target_depth_values[max_depth_index[0]].item()
|
| 15 |
+
|
| 16 |
+
# Compute the normalized x and y coordinates
|
| 17 |
+
x, y = max_depth_index
|
| 18 |
+
x_normalized = (x / (width - 1)) * 2 - 1
|
| 19 |
+
y_normalized = (y / (height - 1)) * 2 - 1
|
| 20 |
+
|
| 21 |
+
# Calculate horizontal and vertical field of view (in radians)
|
| 22 |
+
h_fov_rad = np.radians(h_fov)
|
| 23 |
+
aspect_ratio = width / height
|
| 24 |
+
v_fov_rad = h_fov_rad / aspect_ratio
|
| 25 |
+
|
| 26 |
+
# Calculate the world coordinates (x, y) at the target depth
|
| 27 |
+
x_world = np.tan(h_fov_rad / 2) * max_depth * x_normalized
|
| 28 |
+
y_world = np.tan(v_fov_rad / 2) * max_depth * y_normalized
|
| 29 |
+
|
| 30 |
+
# Compute the target position using the world coordinates and max_depth
|
| 31 |
+
target_position = np.array([x_world, y_world, max_depth])
|
| 32 |
+
|
| 33 |
+
# Assuming the camera is initially at the origin, and looking in the negative Z direction
|
| 34 |
+
cam_position = np.array([0, 0, 0])
|
| 35 |
+
current_direction = np.array([0, 0, -1])
|
| 36 |
+
|
| 37 |
+
# Compute the direction vector and normalize it
|
| 38 |
+
direction = target_position - cam_position
|
| 39 |
+
direction = direction / np.linalg.norm(direction)
|
| 40 |
+
|
| 41 |
+
# Compute the rotation angle based on the turn_weight (number of frames)
|
| 42 |
+
axis = np.cross(current_direction, direction)
|
| 43 |
+
axis = axis / np.linalg.norm(axis)
|
| 44 |
+
angle = np.arcsin(np.linalg.norm(axis))
|
| 45 |
+
max_angle = np.pi * (0.1 / turn_weight) # Limit the maximum rotation angle to half of the visible screen
|
| 46 |
+
rotation_angle = np.clip(np.sign(np.cross(current_direction, direction)) * angle / turn_weight, -max_angle, max_angle)
|
| 47 |
+
|
| 48 |
+
# Compute the rotation matrix
|
| 49 |
+
rotation_matrix = np.eye(3) + np.sin(rotation_angle) * np.array([
|
| 50 |
+
[0, -axis[2], axis[1]],
|
| 51 |
+
[axis[2], 0, -axis[0]],
|
| 52 |
+
[-axis[1], axis[0], 0]
|
| 53 |
+
]) + (1 - np.cos(rotation_angle)) * np.outer(axis, axis)
|
| 54 |
+
|
| 55 |
+
# Convert the NumPy array to a PyTorch tensor
|
| 56 |
+
rotation_matrix_tensor = torch.from_numpy(rotation_matrix).float()
|
| 57 |
+
|
| 58 |
+
# Add an extra dimension to match the expected shape (1, 3, 3)
|
| 59 |
+
rotation_matrix_tensor = rotation_matrix_tensor.unsqueeze(0)
|
| 60 |
+
|
| 61 |
+
return rotation_matrix_tensor
|
| 62 |
+
|
| 63 |
+
def rotation_matrix(axis, angle):
|
| 64 |
+
axis = np.asarray(axis)
|
| 65 |
+
axis = axis / np.linalg.norm(axis)
|
| 66 |
+
a = np.cos(angle / 2.0)
|
| 67 |
+
b, c, d = -axis * np.sin(angle / 2.0)
|
| 68 |
+
aa, bb, cc, dd = a * a, b * b, c * c, d * d
|
| 69 |
+
bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
|
| 70 |
+
return np.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
|
| 71 |
+
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
|
| 72 |
+
[2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
|
scripts/deforum_helpers/colors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import pkg_resources
|
| 3 |
+
from skimage.exposure import match_histograms
|
| 4 |
+
|
| 5 |
+
def maintain_colors(prev_img, color_match_sample, mode):
|
| 6 |
+
skimage_version = pkg_resources.get_distribution('scikit-image').version
|
| 7 |
+
is_skimage_v20_or_higher = pkg_resources.parse_version(skimage_version) >= pkg_resources.parse_version('0.20.0')
|
| 8 |
+
|
| 9 |
+
match_histograms_kwargs = {'channel_axis': -1} if is_skimage_v20_or_higher else {'multichannel': True}
|
| 10 |
+
|
| 11 |
+
if mode == 'RGB':
|
| 12 |
+
return match_histograms(prev_img, color_match_sample, **match_histograms_kwargs)
|
| 13 |
+
elif mode == 'HSV':
|
| 14 |
+
prev_img_hsv = cv2.cvtColor(prev_img, cv2.COLOR_RGB2HSV)
|
| 15 |
+
color_match_hsv = cv2.cvtColor(color_match_sample, cv2.COLOR_RGB2HSV)
|
| 16 |
+
matched_hsv = match_histograms(prev_img_hsv, color_match_hsv, **match_histograms_kwargs)
|
| 17 |
+
return cv2.cvtColor(matched_hsv, cv2.COLOR_HSV2RGB)
|
| 18 |
+
else: # LAB
|
| 19 |
+
prev_img_lab = cv2.cvtColor(prev_img, cv2.COLOR_RGB2LAB)
|
| 20 |
+
color_match_lab = cv2.cvtColor(color_match_sample, cv2.COLOR_RGB2LAB)
|
| 21 |
+
matched_lab = match_histograms(prev_img_lab, color_match_lab, **match_histograms_kwargs)
|
| 22 |
+
return cv2.cvtColor(matched_lab, cv2.COLOR_LAB2RGB)
|
scripts/deforum_helpers/composable_masks.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# At the moment there are three types of masks: mask from variable, file mask and word mask
|
| 2 |
+
# Variable masks include video_mask (which can be set to auto-generated human masks) and everywhere
|
| 3 |
+
# They are put in {}-brackets
|
| 4 |
+
# Word masks are framed with <>-bracets, like: <cat>, <anime girl>
|
| 5 |
+
# File masks are put in []-brackes
|
| 6 |
+
# Empty strings are counted as the whole frame
|
| 7 |
+
# We want to put them all into a sequence of boolean operations
|
| 8 |
+
|
| 9 |
+
# Example:
|
| 10 |
+
# \ <armor>
|
| 11 |
+
# (({human_mask} & [mask1.png]) ^ <apple>)
|
| 12 |
+
|
| 13 |
+
# Writing the parser for the boolean sequence
|
| 14 |
+
# using regex and PIL operations
|
| 15 |
+
import re
|
| 16 |
+
from .load_images import get_mask_from_file, check_mask_for_errors, blank_if_none
|
| 17 |
+
from .word_masking import get_word_mask
|
| 18 |
+
from PIL import ImageChops
|
| 19 |
+
from modules.shared import opts
|
| 20 |
+
|
| 21 |
+
# val_masks: name, PIL Image mask
|
| 22 |
+
# Returns an image in mode '1' (needed for bool ops), convert to 'L' in the sender function
|
| 23 |
+
def compose_mask(root, args, mask_seq, val_masks, frame_image, inner_idx:int = 0):
|
| 24 |
+
# Compose_mask recursively: go to inner brackets, then b-op it and go upstack
|
| 25 |
+
|
| 26 |
+
# Step 1:
|
| 27 |
+
# recursive parenthesis pass
|
| 28 |
+
# regex is not powerful here
|
| 29 |
+
|
| 30 |
+
seq = ""
|
| 31 |
+
inner_seq = ""
|
| 32 |
+
parentheses_counter = 0
|
| 33 |
+
|
| 34 |
+
for c in mask_seq:
|
| 35 |
+
if c == ')':
|
| 36 |
+
parentheses_counter = parentheses_counter - 1
|
| 37 |
+
if parentheses_counter > 0:
|
| 38 |
+
inner_seq += c
|
| 39 |
+
if c == '(':
|
| 40 |
+
parentheses_counter = parentheses_counter + 1
|
| 41 |
+
if parentheses_counter == 0:
|
| 42 |
+
if len(inner_seq) > 0:
|
| 43 |
+
inner_idx += 1
|
| 44 |
+
seq += compose_mask(root, args, inner_seq, val_masks, frame_image, inner_idx)
|
| 45 |
+
inner_seq = ""
|
| 46 |
+
else:
|
| 47 |
+
seq += c
|
| 48 |
+
|
| 49 |
+
if parentheses_counter != 0:
|
| 50 |
+
raise Exception('Mismatched parentheses in {mask_seq}!')
|
| 51 |
+
|
| 52 |
+
mask_seq = seq
|
| 53 |
+
|
| 54 |
+
# Step 2:
|
| 55 |
+
# Load the word masks and file masks as vars
|
| 56 |
+
|
| 57 |
+
# File masks
|
| 58 |
+
pattern = r'\[(?P<inner>[\S\s]*?)\]'
|
| 59 |
+
|
| 60 |
+
def parse(match_object):
|
| 61 |
+
nonlocal inner_idx
|
| 62 |
+
inner_idx += 1
|
| 63 |
+
content = match_object.groupdict()['inner']
|
| 64 |
+
val_masks[str(inner_idx)] = get_mask_from_file(content, args).convert('1') # TODO: add caching
|
| 65 |
+
return f"{{{inner_idx}}}"
|
| 66 |
+
|
| 67 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 68 |
+
|
| 69 |
+
# Word masks
|
| 70 |
+
pattern = r'<(?P<inner>[\S\s]*?)>'
|
| 71 |
+
|
| 72 |
+
def parse(match_object):
|
| 73 |
+
nonlocal inner_idx
|
| 74 |
+
inner_idx += 1
|
| 75 |
+
content = match_object.groupdict()['inner']
|
| 76 |
+
val_masks[str(inner_idx)] = get_word_mask(root, frame_image, content).convert('1')
|
| 77 |
+
return f"{{{inner_idx}}}"
|
| 78 |
+
|
| 79 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 80 |
+
|
| 81 |
+
# Now that all inner parenthesis are eliminated we're left with a linear string
|
| 82 |
+
|
| 83 |
+
# Step 3:
|
| 84 |
+
# Boolean operations with masks
|
| 85 |
+
# Operators: invert !, and &, or |, xor ^, difference \
|
| 86 |
+
|
| 87 |
+
# Invert vars with '!'
|
| 88 |
+
pattern = r'![\S\s]*{(?P<inner>[\S\s]*?)}'
|
| 89 |
+
def parse(match_object):
|
| 90 |
+
nonlocal inner_idx
|
| 91 |
+
inner_idx += 1
|
| 92 |
+
content = match_object.groupdict()['inner']
|
| 93 |
+
savename = content
|
| 94 |
+
if content in root.mask_preset_names:
|
| 95 |
+
inner_idx += 1
|
| 96 |
+
savename = str(inner_idx)
|
| 97 |
+
val_masks[savename] = ImageChops.invert(val_masks[content])
|
| 98 |
+
return f"{{{savename}}}"
|
| 99 |
+
|
| 100 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 101 |
+
|
| 102 |
+
# Multiply neighbouring vars with '&'
|
| 103 |
+
# Wait for replacements stall (like in Markov chains)
|
| 104 |
+
while True:
|
| 105 |
+
pattern = r'{(?P<inner1>[\S\s]*?)}[\s]*&[\s]*{(?P<inner2>[\S\s]*?)}'
|
| 106 |
+
def parse(match_object):
|
| 107 |
+
nonlocal inner_idx
|
| 108 |
+
inner_idx += 1
|
| 109 |
+
content = match_object.groupdict()['inner1']
|
| 110 |
+
content_second = match_object.groupdict()['inner2']
|
| 111 |
+
savename = content
|
| 112 |
+
if content in root.mask_preset_names:
|
| 113 |
+
inner_idx += 1
|
| 114 |
+
savename = str(inner_idx)
|
| 115 |
+
val_masks[savename] = ImageChops.logical_and(val_masks[content], val_masks[content_second])
|
| 116 |
+
return f"{{{savename}}}"
|
| 117 |
+
|
| 118 |
+
prev_mask_seq = mask_seq
|
| 119 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 120 |
+
if mask_seq is prev_mask_seq:
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
# Add neighbouring vars with '|'
|
| 124 |
+
while True:
|
| 125 |
+
pattern = r'{(?P<inner1>[\S\s]*?)}[\s]*?\|[\s]*?{(?P<inner2>[\S\s]*?)}'
|
| 126 |
+
def parse(match_object):
|
| 127 |
+
nonlocal inner_idx
|
| 128 |
+
inner_idx += 1
|
| 129 |
+
content = match_object.groupdict()['inner1']
|
| 130 |
+
content_second = match_object.groupdict()['inner2']
|
| 131 |
+
savename = content
|
| 132 |
+
if content in root.mask_preset_names:
|
| 133 |
+
inner_idx += 1
|
| 134 |
+
savename = str(inner_idx)
|
| 135 |
+
val_masks[savename] = ImageChops.logical_or(val_masks[content], val_masks[content_second])
|
| 136 |
+
return f"{{{savename}}}"
|
| 137 |
+
|
| 138 |
+
prev_mask_seq = mask_seq
|
| 139 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 140 |
+
if mask_seq is prev_mask_seq:
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
# Mutually exclude neighbouring vars with '^'
|
| 144 |
+
while True:
|
| 145 |
+
pattern = r'{(?P<inner1>[\S\s]*?)}[\s]*\^[\s]*{(?P<inner2>[\S\s]*?)}'
|
| 146 |
+
def parse(match_object):
|
| 147 |
+
nonlocal inner_idx
|
| 148 |
+
inner_idx += 1
|
| 149 |
+
content = match_object.groupdict()['inner1']
|
| 150 |
+
content_second = match_object.groupdict()['inner2']
|
| 151 |
+
savename = content
|
| 152 |
+
if content in root.mask_preset_names:
|
| 153 |
+
inner_idx += 1
|
| 154 |
+
savename = str(inner_idx)
|
| 155 |
+
val_masks[savename] = ImageChops.logical_xor(val_masks[content], val_masks[content_second])
|
| 156 |
+
return f"{{{savename}}}"
|
| 157 |
+
|
| 158 |
+
prev_mask_seq = mask_seq
|
| 159 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 160 |
+
if mask_seq is prev_mask_seq:
|
| 161 |
+
break
|
| 162 |
+
|
| 163 |
+
# Set-difference the regions with '\'
|
| 164 |
+
while True:
|
| 165 |
+
pattern = r'{(?P<inner1>[\S\s]*?)}[\s]*\\[\s]*{(?P<inner2>[\S\s]*?)}'
|
| 166 |
+
def parse(match_object):
|
| 167 |
+
content = match_object.groupdict()['inner1']
|
| 168 |
+
content_second = match_object.groupdict()['inner2']
|
| 169 |
+
savename = content
|
| 170 |
+
if content in root.mask_preset_names:
|
| 171 |
+
nonlocal inner_idx
|
| 172 |
+
inner_idx += 1
|
| 173 |
+
savename = str(inner_idx)
|
| 174 |
+
val_masks[savename] = ImageChops.logical_and(val_masks[content], ImageChops.invert(val_masks[content_second]))
|
| 175 |
+
return f"{{{savename}}}"
|
| 176 |
+
|
| 177 |
+
prev_mask_seq = mask_seq
|
| 178 |
+
mask_seq = re.sub(pattern, parse, mask_seq)
|
| 179 |
+
if mask_seq is prev_mask_seq:
|
| 180 |
+
break
|
| 181 |
+
|
| 182 |
+
# Step 4:
|
| 183 |
+
# Output
|
| 184 |
+
# Now we should have a single var left to return. If not, raise an error message
|
| 185 |
+
pattern = r'{(?P<inner>[\S\s]*?)}'
|
| 186 |
+
matches = re.findall(pattern, mask_seq)
|
| 187 |
+
|
| 188 |
+
if len(matches) != 1:
|
| 189 |
+
raise Exception(f'Wrong composable mask expression format! Broken mask sequence: {mask_seq}')
|
| 190 |
+
|
| 191 |
+
return f"{{{matches[0]}}}"
|
| 192 |
+
|
| 193 |
+
def compose_mask_with_check(root, args, mask_seq, val_masks, frame_image):
|
| 194 |
+
for k, v in val_masks.items():
|
| 195 |
+
val_masks[k] = blank_if_none(v, args.W, args.H, '1').convert('1')
|
| 196 |
+
return check_mask_for_errors(val_masks[compose_mask(root, args, mask_seq, val_masks, frame_image, 0)[1:-1]].convert('L'))
|
scripts/deforum_helpers/consistency_check.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
Taken from https://github.com/Sxela/flow_tools/blob/main
|
| 3 |
+
'''
|
| 4 |
+
# import argparse
|
| 5 |
+
# import PIL.Image
|
| 6 |
+
import numpy as np
|
| 7 |
+
# import scipy.ndimage
|
| 8 |
+
# import glob
|
| 9 |
+
# from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
def make_consistency(flow1, flow2, edges_unreliable=False):
|
| 12 |
+
# Awesome pythonic consistency check from [maua](https://github.com/maua-maua-maua/maua/blob/44485c745c65cf9d83cb1b1c792a177588e9c9fc/maua/flow/consistency.py) by Hans Brouwer and Henry Rachootin
|
| 13 |
+
# algorithm based on https://github.com/manuelruder/artistic-videos/blob/master/consistencyChecker/consistencyChecker.cpp
|
| 14 |
+
# reimplemented in numpy by Hans Brouwer
|
| 15 |
+
# // consistencyChecker
|
| 16 |
+
# // Check consistency of forward flow via backward flow.
|
| 17 |
+
# // (c) Manuel Ruder, Alexey Dosovitskiy, Thomas Brox 2016
|
| 18 |
+
|
| 19 |
+
flow1 = np.flip(flow1, axis=2)
|
| 20 |
+
flow2 = np.flip(flow2, axis=2)
|
| 21 |
+
h, w, _ = flow1.shape
|
| 22 |
+
|
| 23 |
+
# get grid of coordinates for each pixel
|
| 24 |
+
orig_coord = np.flip(np.mgrid[:w, :h], 0).T
|
| 25 |
+
|
| 26 |
+
# find where the flow1 maps each pixel
|
| 27 |
+
warp_coord = orig_coord + flow1
|
| 28 |
+
|
| 29 |
+
# clip the coordinates in bounds and round down
|
| 30 |
+
warp_coord_inbound = np.zeros_like(warp_coord)
|
| 31 |
+
warp_coord_inbound[..., 0] = np.clip(warp_coord[..., 0], 0, h - 2)
|
| 32 |
+
warp_coord_inbound[..., 1] = np.clip(warp_coord[..., 1], 0, w - 2)
|
| 33 |
+
warp_coord_floor = np.floor(warp_coord_inbound).astype(int)
|
| 34 |
+
|
| 35 |
+
# for each pixel: bilinear interpolation of the corresponding flow2 values around the point mapped to by flow1
|
| 36 |
+
alpha = warp_coord_inbound - warp_coord_floor
|
| 37 |
+
flow2_00 = flow2[warp_coord_floor[..., 0], warp_coord_floor[..., 1]]
|
| 38 |
+
flow2_01 = flow2[warp_coord_floor[..., 0], warp_coord_floor[..., 1] + 1]
|
| 39 |
+
flow2_10 = flow2[warp_coord_floor[..., 0] + 1, warp_coord_floor[..., 1]]
|
| 40 |
+
flow2_11 = flow2[warp_coord_floor[..., 0] + 1, warp_coord_floor[..., 1] + 1]
|
| 41 |
+
flow2_0_blend = (1 - alpha[..., 1, None]) * flow2_00 + alpha[..., 1, None] * flow2_01
|
| 42 |
+
flow2_1_blend = (1 - alpha[..., 1, None]) * flow2_10 + alpha[..., 1, None] * flow2_11
|
| 43 |
+
warp_coord_flow2 = (1 - alpha[..., 0, None]) * flow2_0_blend + alpha[..., 0, None] * flow2_1_blend
|
| 44 |
+
|
| 45 |
+
# coordinates that flow2 remaps each flow1-mapped pixel to
|
| 46 |
+
rewarp_coord = warp_coord + warp_coord_flow2
|
| 47 |
+
|
| 48 |
+
# where the difference in position after flow1 and flow2 are applied is larger than a threshold there is likely an
|
| 49 |
+
# occlusion. set values to -1 so the final gaussian blur will spread the value a couple pixels around this area
|
| 50 |
+
squared_diff = np.sum((rewarp_coord - orig_coord) ** 2, axis=2)
|
| 51 |
+
threshold = 0.01 * np.sum(warp_coord_flow2 ** 2 + flow1 ** 2, axis=2) + 0.5
|
| 52 |
+
|
| 53 |
+
reliable_flow = np.ones((squared_diff.shape[0], squared_diff.shape[1], 3))
|
| 54 |
+
reliable_flow[...,0] = np.where(squared_diff >= threshold, -0.75, 1)
|
| 55 |
+
|
| 56 |
+
# areas mapping outside of the frame are also occluded (don't need extra region around these though, so set 0)
|
| 57 |
+
if edges_unreliable:
|
| 58 |
+
reliable_flow[...,1] = np.where(
|
| 59 |
+
np.logical_or.reduce(
|
| 60 |
+
(
|
| 61 |
+
warp_coord[..., 0] < 0,
|
| 62 |
+
warp_coord[..., 1] < 0,
|
| 63 |
+
warp_coord[..., 0] >= h - 1,
|
| 64 |
+
warp_coord[..., 1] >= w - 1,
|
| 65 |
+
)
|
| 66 |
+
),
|
| 67 |
+
0,
|
| 68 |
+
reliable_flow[...,1],
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# get derivative of flow, large changes in derivative => edge of moving object
|
| 72 |
+
dx = np.diff(flow1, axis=1, append=0)
|
| 73 |
+
dy = np.diff(flow1, axis=0, append=0)
|
| 74 |
+
motion_edge = np.sum(dx ** 2 + dy ** 2, axis=2)
|
| 75 |
+
motion_threshold = 0.01 * np.sum(flow1 ** 2, axis=2) + 0.002
|
| 76 |
+
reliable_flow[...,2] = np.where(np.logical_and(motion_edge > motion_threshold, reliable_flow[...,2] != -0.75), 0, reliable_flow[...,2])
|
| 77 |
+
|
| 78 |
+
return reliable_flow
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# parser = argparse.ArgumentParser()
|
| 82 |
+
# parser.add_argument("--flow_fwd", type=str, required=True, help="Forward flow path or glob pattern")
|
| 83 |
+
# parser.add_argument("--flow_bwd", type=str, required=True, help="Backward flow path or glob pattern")
|
| 84 |
+
# parser.add_argument("--output", type=str, required=True, help="Output consistency map path")
|
| 85 |
+
# parser.add_argument("--output_postfix", type=str, default='_cc', help="Output consistency map name postfix")
|
| 86 |
+
# parser.add_argument("--image_output", action='store_true', help="Output consistency map as b\w image path")
|
| 87 |
+
# parser.add_argument("--skip_numpy_output", action='store_true', help="Don`t save numpy array")
|
| 88 |
+
# parser.add_argument("--blur", type=float, default=2., help="Gaussian blur kernel size (0 for no blur)")
|
| 89 |
+
# parser.add_argument("--bottom_clamp", type=float, default=0., help="Clamp lower values")
|
| 90 |
+
# parser.add_argument("--edges_reliable", action='store_true', help="Consider edges reliable")
|
| 91 |
+
# parser.add_argument("--save_separate_channels", action='store_true', help="Save consistency mask layers as separate channels")
|
| 92 |
+
# args = parser.parse_args()
|
| 93 |
+
|
| 94 |
+
# def run(args):
|
| 95 |
+
# flow_fwd_many = sorted(glob.glob(args.flow_fwd))
|
| 96 |
+
# flow_bwd_many = sorted(glob.glob(args.flow_bwd))
|
| 97 |
+
# if len(flow_fwd_many)!= len(flow_bwd_many):
|
| 98 |
+
# raise Exception('Forward and backward flow file numbers don`t match')
|
| 99 |
+
# return
|
| 100 |
+
|
| 101 |
+
# for flow_fwd,flow_bwd in tqdm(zip(flow_fwd_many, flow_bwd_many)):
|
| 102 |
+
# flow_fwd = flow_fwd.replace('\\','/')
|
| 103 |
+
# flow_bwd = flow_bwd.replace('\\','/')
|
| 104 |
+
# flow1 = np.load(flow_fwd)
|
| 105 |
+
# flow2 = np.load(flow_bwd)
|
| 106 |
+
# consistency_map_multilayer = make_consistency(flow1, flow2, edges_unreliable=not args.edges_reliable)
|
| 107 |
+
|
| 108 |
+
# if args.save_separate_channels:
|
| 109 |
+
# consistency_map = consistency_map_multilayer
|
| 110 |
+
# else:
|
| 111 |
+
# consistency_map = np.ones_like(consistency_map_multilayer[...,0])
|
| 112 |
+
# consistency_map*=consistency_map_multilayer[...,0]
|
| 113 |
+
# consistency_map*=consistency_map_multilayer[...,1]
|
| 114 |
+
# consistency_map*=consistency_map_multilayer[...,2]
|
| 115 |
+
|
| 116 |
+
# # blur
|
| 117 |
+
# if args.blur>0.:
|
| 118 |
+
# consistency_map = scipy.ndimage.gaussian_filter(consistency_map, [args.blur, args.blur])
|
| 119 |
+
|
| 120 |
+
# #clip values between bottom_clamp and 1
|
| 121 |
+
# bottom_clamp = min(max(args.bottom_clamp,0.), 0.999)
|
| 122 |
+
# consistency_map = consistency_map.clip(bottom_clamp, 1)
|
| 123 |
+
# out_fname = args.output+'/'+flow_fwd.split('/')[-1][:-4]+args.output_postfix
|
| 124 |
+
|
| 125 |
+
# if not args.skip_numpy_output:
|
| 126 |
+
# np.save(out_fname, consistency_map)
|
| 127 |
+
|
| 128 |
+
# #save as jpeg
|
| 129 |
+
# if args.image_output:
|
| 130 |
+
# PIL.Image.fromarray((consistency_map*255.).astype('uint8')).save(out_fname+'.jpg', quality=90)
|
| 131 |
+
|
| 132 |
+
# run(args)
|
scripts/deforum_helpers/defaults.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def get_samplers_list():
|
| 2 |
+
return {
|
| 3 |
+
'euler a': 'Euler a',
|
| 4 |
+
'euler': 'Euler',
|
| 5 |
+
'lms': 'LMS',
|
| 6 |
+
'heun': 'Heun',
|
| 7 |
+
'dpm2': 'DPM2',
|
| 8 |
+
'dpm2 a': 'DPM2 a',
|
| 9 |
+
'dpm++ 2s a': 'DPM++ 2S a',
|
| 10 |
+
'dpm++ 2m': 'DPM++ 2M',
|
| 11 |
+
'dpm++ sde': 'DPM++ SDE',
|
| 12 |
+
'dpm fast': 'DPM fast',
|
| 13 |
+
'dpm adaptive': 'DPM adaptive',
|
| 14 |
+
'lms karras': 'LMS Karras',
|
| 15 |
+
'dpm2 karras': 'DPM2 Karras',
|
| 16 |
+
'dpm2 a karras': 'DPM2 a Karras',
|
| 17 |
+
'dpm++ 2s a karras': 'DPM++ 2S a Karras',
|
| 18 |
+
'dpm++ 2m karras': 'DPM++ 2M Karras',
|
| 19 |
+
'dpm++ sde karras': 'DPM++ SDE Karras'
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def DeforumAnimPrompts():
|
| 23 |
+
return r"""{
|
| 24 |
+
"0": "tiny cute bunny, vibrant diffraction, highly detailed, intricate, ultra hd, sharp photo, crepuscular rays, in focus, by tomasz alen kopera",
|
| 25 |
+
"30": "anthropomorphic clean cat, surrounded by fractals, epic angle and pose, symmetrical, 3d, depth of field, ruan jia and fenghua zhong",
|
| 26 |
+
"60": "a beautiful coconut --neg photo, realistic",
|
| 27 |
+
"90": "a beautiful durian, trending on Artstation"
|
| 28 |
+
}
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
# Guided images defaults
|
| 32 |
+
def get_guided_imgs_default_json():
|
| 33 |
+
return '''{
|
| 34 |
+
"0": "https://deforum.github.io/a1/Gi1.png",
|
| 35 |
+
"max_f/4-5": "https://deforum.github.io/a1/Gi2.png",
|
| 36 |
+
"max_f/2-10": "https://deforum.github.io/a1/Gi3.png",
|
| 37 |
+
"3*max_f/4-15": "https://deforum.github.io/a1/Gi4.jpg",
|
| 38 |
+
"max_f-20": "https://deforum.github.io/a1/Gi1.png"
|
| 39 |
+
}'''
|
| 40 |
+
|
| 41 |
+
def get_hybrid_info_html():
|
| 42 |
+
return """
|
| 43 |
+
<p style="padding-bottom:0">
|
| 44 |
+
<b style="text-shadow: blue -1px -1px;">Hybrid Video Compositing in 2D/3D Mode</b>
|
| 45 |
+
<span style="color:#DDD;font-size:0.7rem;text-shadow: black -1px -1px;margin-left:10px;">
|
| 46 |
+
by <a href="https://github.com/reallybigname">reallybigname</a>
|
| 47 |
+
</span>
|
| 48 |
+
</p>
|
| 49 |
+
<ul style="list-style-type:circle; margin-left:1em; margin-bottom:1em;">
|
| 50 |
+
<li>Composite video with previous frame init image in <b>2D or 3D animation_mode</b> <i>(not for Video Input mode)</i></li>
|
| 51 |
+
<li>Uses your <b>Init</b> settings for <b>video_init_path, extract_nth_frame, overwrite_extracted_frames</b></li>
|
| 52 |
+
<li>In Keyframes tab, you can also set <b>color_coherence</b> = '<b>Video Input</b>'</li>
|
| 53 |
+
<li><b>color_coherence_video_every_N_frames</b> lets you only match every N frames</li>
|
| 54 |
+
<li>Color coherence may be used with hybrid composite off, to just use video color.</li>
|
| 55 |
+
<li>Hybrid motion may be used with hybrid composite off, to just use video motion.</li>
|
| 56 |
+
</ul>
|
| 57 |
+
Hybrid Video Schedules
|
| 58 |
+
<ul style="list-style-type:circle; margin-left:1em; margin-bottom:1em;">
|
| 59 |
+
<li>The alpha schedule controls overall alpha for video mix, whether using a composite mask or not.</li>
|
| 60 |
+
<li>The <b>hybrid_comp_mask_blend_alpha_schedule</b> only affects the 'Blend' <b>hybrid_comp_mask_type</b>.</li>
|
| 61 |
+
<li>Mask contrast schedule is from 0-255. Normal is 1. Affects all masks.</li>
|
| 62 |
+
<li>Autocontrast low/high cutoff schedules 0-100. Low 0 High 100 is full range. <br>(<i><b>hybrid_comp_mask_auto_contrast</b> must be enabled</i>)</li>
|
| 63 |
+
</ul>
|
| 64 |
+
<a style='color:SteelBlue;' target='_blank' href='https://github.com/deforum-art/deforum-for-automatic1111-webui/wiki/Animation-Settings#hybrid-video-mode-for-2d3d-animations'>Click Here</a> for more info/ a Guide.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def get_composable_masks_info_html():
|
| 68 |
+
return """
|
| 69 |
+
<ul style="list-style-type:circle; margin-left:0.75em; margin-bottom:0.2em">
|
| 70 |
+
<li>To enable, check use_mask in the Init tab</li>
|
| 71 |
+
<li>Supports boolean operations: (! - negation, & - and, | - or, ^ - xor, \ - difference, () - nested operations)</li>
|
| 72 |
+
<li>default variables: in \{\}, like \{init_mask\}, \{video_mask\}, \{everywhere\}</li>
|
| 73 |
+
<li>masks from files: in [], like [mask1.png]</li>
|
| 74 |
+
<li>description-based: <i>word masks</i> in <>, like <apple>, <hair></li>
|
| 75 |
+
</ul>
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
def get_parseq_info_html():
|
| 79 |
+
return """
|
| 80 |
+
<p>Use a <a style='color:SteelBlue;' target='_blank' href='https://sd-parseq.web.app/deforum'>Parseq</a> manifest for your animation (leave blank to ignore).</p>
|
| 81 |
+
<p style="margin-top:1em; margin-bottom:1em;">
|
| 82 |
+
Fields managed in your Parseq manifest override the values and schedules set in other parts of this UI. You can select which values to override by using the "Managed Fields" section in Parseq.
|
| 83 |
+
</p>
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
def get_prompts_info_html():
|
| 87 |
+
return """
|
| 88 |
+
<ul style="list-style-type:circle; margin-left:0.75em; margin-bottom:0.2em">
|
| 89 |
+
<li>Please always keep values in math functions above 0.</li>
|
| 90 |
+
<li>There is *no* Batch mode like in vanilla deforum. Please Use the txt2img tab for that.</li>
|
| 91 |
+
<li>For negative prompts, please write your positive prompt, then --neg ugly, text, assymetric, or any other negative tokens of your choice. OR:</li>
|
| 92 |
+
<li>Use the negative_prompts field to automatically append all words as a negative prompt. *Don't* add --neg in the negative_prompts field!</li>
|
| 93 |
+
<li>Prompts are stored in JSON format. If you've got an error, check it in a <a style="color:SteelBlue" href="https://odu.github.io/slingjsonlint/">JSON Validator</a></li>
|
| 94 |
+
</ul>
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def get_guided_imgs_info_html():
|
| 98 |
+
return """
|
| 99 |
+
<p>You can use this as a guided image tool or as a looper depending on your settings in the keyframe images field.
|
| 100 |
+
Set the keyframes and the images that you want to show up.
|
| 101 |
+
Note: the number of frames between each keyframe should be greater than the tweening frames.</p>
|
| 102 |
+
|
| 103 |
+
<p>Prerequisites and Important Info:</p>
|
| 104 |
+
<ul style="list-style-type:circle; margin-left:2em; margin-bottom:0em">
|
| 105 |
+
<li>This mode works ONLY with 2D/3D animation modes. Interpolation and Video Input modes aren't supported.</li>
|
| 106 |
+
<li>Init tab's strength slider should be greater than 0. Recommended value (.65 - .80).</li>
|
| 107 |
+
<li>'seed_behavior' will be forcibly set to 'schedule'.</li>
|
| 108 |
+
</ul>
|
| 109 |
+
|
| 110 |
+
<p>Looping recommendations:</p>
|
| 111 |
+
<ul style="list-style-type:circle; margin-left:2em; margin-bottom:0em">
|
| 112 |
+
<li>seed_schedule should start and end on the same seed.<br />
|
| 113 |
+
Example: seed_schedule could use 0:(5), 1:(-1), 219:(-1), 220:(5)</li>
|
| 114 |
+
<li>The 1st and last keyframe images should match.</li>
|
| 115 |
+
<li>Set your total number of keyframes to be 21 more than the last inserted keyframe image.<br />
|
| 116 |
+
Example: Default args should use 221 as the total keyframes.</li>
|
| 117 |
+
<li>Prompts are stored in JSON format. If you've got an error, check it in the validator,
|
| 118 |
+
<a style="color:SteelBlue" href="https://odu.github.io/slingjsonlint/">like here</a></li>
|
| 119 |
+
</ul>
|
| 120 |
+
|
| 121 |
+
<p>The Guided images mode exposes the following variables for the prompts and the schedules:</p>
|
| 122 |
+
<ul style="list-style-type:circle; margin-left:2em; margin-bottom:0em">
|
| 123 |
+
<li><b>s</b> is the <i>initial</i> seed for the whole video generation.</li>
|
| 124 |
+
<li><b>max_f</b> is the length of the video, in frames.<br />
|
| 125 |
+
Example: seed_schedule could use 0:(s), 1:(-1), "max_f-2":(-1), "max_f-1":(s)</li>
|
| 126 |
+
<li><b>t</b> is the current frame number.<br />
|
| 127 |
+
Example: strength_schedule could use 0:(0.25 * cos((72 / 60 * 3.141 * (t + 0) / 30))**13 + 0.7) to make alternating changes each 30 frames</li>
|
| 128 |
+
</ul>
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
def get_main_info_html():
|
| 132 |
+
return """
|
| 133 |
+
<p><strong>Made by <a href="https://deforum.github.io">deforum.github.io</a>, port for AUTOMATIC1111's webui maintained by <a href="https://github.com/kabachuha">kabachuha</a></strong> & <a href="https://github.com/hithereai">hithereai</a></strong></p>
|
| 134 |
+
<p><a style="color:SteelBlue" href="https://github.com/deforum-art/deforum-for-automatic1111-webui/wiki/FAQ-&-Troubleshooting">FOR HELP CLICK HERE</a></p>
|
| 135 |
+
<ul style="list-style-type:circle; margin-left:1em">
|
| 136 |
+
<li>The code for this extension: <a style="color:SteelBlue" href="https://github.com/deforum-art/deforum-for-automatic1111-webui">here</a>.</li>
|
| 137 |
+
<li>Join the <a style="color:SteelBlue" href="https://discord.gg/deforum">official Deforum Discord</a> to share your creations and suggestions.</li>
|
| 138 |
+
<li>Official Deforum Wiki: <a style="color:SteelBlue" href="https://github.com/deforum-art/deforum-for-automatic1111-webui/wiki">here</a>.</li>
|
| 139 |
+
<li>Anime-inclined great guide (by FizzleDorf) with lots of examples: <a style="color:SteelBlue" href="https://rentry.org/AnimAnon-Deforum">here</a>.</li>
|
| 140 |
+
<li>For advanced keyframing with Math functions, see <a style="color:SteelBlue" href="https://github.com/deforum-art/deforum-for-automatic1111-webui/wiki/Maths-in-Deforum">here</a>.</li>
|
| 141 |
+
<li>Alternatively, use <a style="color:SteelBlue" href="https://sd-parseq.web.app/deforum">sd-parseq</a> as a UI to define your animation schedules (see the Parseq section in the Init tab).</li>
|
| 142 |
+
<li><a style="color:SteelBlue" href="https://www.framesync.xyz/">framesync.xyz</a> is also a good option, it makes compact math formulae for Deforum keyframes by selecting various waveforms.</li>
|
| 143 |
+
<li>The other site allows for making keyframes using <a style="color:SteelBlue" href="https://www.chigozie.co.uk/keyframe-string-generator/">interactive splines and Bezier curves</a> (select Disco output format).</li>
|
| 144 |
+
<li>If you want to use Width/Height which are not multiples of 64, please change noise_type to 'Uniform', in Keyframes --> Noise.</li>
|
| 145 |
+
</ul>
|
| 146 |
+
<italic>If you liked this extension, please <a style="color:SteelBlue" href="https://github.com/deforum-art/deforum-for-automatic1111-webui">give it a star on GitHub</a>!</italic> 😊
|
| 147 |
+
"""
|
| 148 |
+
def get_frame_interpolation_info_html():
|
| 149 |
+
return """
|
| 150 |
+
Use <a href="https://github.com/megvii-research/ECCV2022-RIFE">RIFE</a> / <a href="https://film-net.github.io/">FILM</a> Frame Interpolation to smooth out, slow-mo (or both) any video.</p>
|
| 151 |
+
<p style="margin-top:1em">
|
| 152 |
+
Supported engines:
|
| 153 |
+
<ul style="list-style-type:circle; margin-left:1em; margin-bottom:1em">
|
| 154 |
+
<li>RIFE v4.6 and FILM.</li>
|
| 155 |
+
</ul>
|
| 156 |
+
</p>
|
| 157 |
+
<p style="margin-top:1em">
|
| 158 |
+
Important notes:
|
| 159 |
+
<ul style="list-style-type:circle; margin-left:1em; margin-bottom:1em">
|
| 160 |
+
<li>Frame Interpolation will *not* run if any of the following are enabled: 'Store frames in ram' / 'Skip video for run all'.</li>
|
| 161 |
+
<li>Audio (if provided) will *not* be transferred to the interpolated video if Slow-Mo is enabled.</li>
|
| 162 |
+
<li>'add_soundtrack' and 'soundtrack_path' aren't being honoured in "Interpolate an existing video" mode. Original vid audio will be used instead with the same slow-mo rules above.</li>
|
| 163 |
+
<li>In "Interpolate existing pics" mode, FPS is determined *only* by output FPS slider. Audio will be added if requested even with slow-mo "enabled", as it does *nothing* in this mode.</li>
|
| 164 |
+
</ul>
|
| 165 |
+
</p>
|
| 166 |
+
"""
|
| 167 |
+
def get_frames_to_video_info_html():
|
| 168 |
+
return """
|
| 169 |
+
<p style="margin-top:0em">
|
| 170 |
+
Important Notes:
|
| 171 |
+
<ul style="list-style-type:circle; margin-left:1em; margin-bottom:0.25em">
|
| 172 |
+
<li>Enter relative to webui folder or Full-Absolute path, and make sure it ends with something like this: '20230124234916_%09d.png', just replace 20230124234916 with your batch ID. The %09d is important, don't forget it!</li>
|
| 173 |
+
<li>In the filename, '%09d' represents the 9 counting numbers, For '20230124234916_000000001.png', use '20230124234916_%09d.png'</li>
|
| 174 |
+
<li>If non-deforum frames, use the correct number of counting digits. For files like 'bunnies-0000.jpg', you'd use 'bunnies-%04d.jpg'</li>
|
| 175 |
+
</ul>
|
| 176 |
+
"""
|
| 177 |
+
def get_leres_info_html():
|
| 178 |
+
return 'Note that LeReS has a Non-Commercial <a href="https://github.com/aim-uofa/AdelaiDepth/blob/main/LeReS/LICENSE" target="_blank">license</a>. Use it only for fun/personal use.'
|
| 179 |
+
|
| 180 |
+
def get_gradio_html(section_name):
|
| 181 |
+
if section_name.lower() == 'hybrid_video':
|
| 182 |
+
return get_hybrid_info_html()
|
| 183 |
+
elif section_name.lower() == 'composable_masks':
|
| 184 |
+
return get_composable_masks_info_html()
|
| 185 |
+
elif section_name.lower() == 'parseq':
|
| 186 |
+
return get_parseq_info_html()
|
| 187 |
+
elif section_name.lower() == 'prompts':
|
| 188 |
+
return get_prompts_info_html()
|
| 189 |
+
elif section_name.lower() == 'guided_imgs':
|
| 190 |
+
return get_guided_imgs_info_html()
|
| 191 |
+
elif section_name.lower() == 'main':
|
| 192 |
+
return get_main_info_html()
|
| 193 |
+
elif section_name.lower() == 'frame_interpolation':
|
| 194 |
+
return get_frame_interpolation_info_html()
|
| 195 |
+
elif section_name.lower() == 'frames_to_video':
|
| 196 |
+
return get_frames_to_video_info_html()
|
| 197 |
+
elif section_name.lower() == 'leres':
|
| 198 |
+
return get_leres_info_html()
|
| 199 |
+
else:
|
| 200 |
+
return ""
|
| 201 |
+
|
| 202 |
+
mask_fill_choices = ['fill', 'original', 'latent noise', 'latent nothing']
|
| 203 |
+
|
scripts/deforum_helpers/deforum_controlnet.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This helper script is responsible for ControlNet/Deforum integration
|
| 2 |
+
# https://github.com/Mikubill/sd-webui-controlnet — controlnet repo
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import scripts
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import numpy as np
|
| 9 |
+
import importlib
|
| 10 |
+
from modules import scripts
|
| 11 |
+
from .deforum_controlnet_gradio import hide_ui_by_cn_status, hide_file_textboxes, ToolButton
|
| 12 |
+
from .general_utils import count_files_in_folder, clean_gradio_path_strings # TODO: do it another way
|
| 13 |
+
from .video_audio_utilities import vid2frames, convert_image
|
| 14 |
+
from .animation_key_frames import ControlNetKeys
|
| 15 |
+
from .load_images import load_image
|
| 16 |
+
from .general_utils import debug_print
|
| 17 |
+
|
| 18 |
+
cnet = None
|
| 19 |
+
# number of CN model tabs to show in the deforum gui
|
| 20 |
+
num_of_models = 5
|
| 21 |
+
|
| 22 |
+
def find_controlnet():
|
| 23 |
+
global cnet
|
| 24 |
+
if cnet: return cnet
|
| 25 |
+
try:
|
| 26 |
+
cnet = importlib.import_module('extensions.sdcontrol.scripts.external_code', 'external_code')
|
| 27 |
+
except:
|
| 28 |
+
try:
|
| 29 |
+
cnet = importlib.import_module('extensions-builtin.sdcontrol.scripts.external_code', 'external_code')
|
| 30 |
+
except:
|
| 31 |
+
pass
|
| 32 |
+
if cnet:
|
| 33 |
+
print(f"\033[0;32m*Deforum ControlNet support: enabled*\033[0m")
|
| 34 |
+
return True
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
def controlnet_infotext():
|
| 38 |
+
return """Requires the <a style='color:SteelBlue;' target='_blank' href='https://github.com/Mikubill/sd-webui-controlnet'>ControlNet</a> extension to be installed.</p>
|
| 39 |
+
<p">If Deforum crashes due to CN updates, go <a style='color:Orange;' target='_blank' href='https://github.com/Mikubill/sd-webui-controlnet/issues'>here</a> and report your problem.</p>
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def is_controlnet_enabled(controlnet_args):
|
| 43 |
+
for i in range(1, num_of_models + 1):
|
| 44 |
+
if getattr(controlnet_args, f'cn_{i}_enabled', False):
|
| 45 |
+
return True
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def setup_controlnet_ui_raw():
|
| 49 |
+
cnet = find_controlnet()
|
| 50 |
+
cn_models = cnet.get_models()
|
| 51 |
+
cn_preprocessors = cnet.get_modules()
|
| 52 |
+
|
| 53 |
+
cn_modules = cnet.get_modules_detail()
|
| 54 |
+
preprocessor_sliders_config = {}
|
| 55 |
+
|
| 56 |
+
for config_name, config_values in cn_modules.items():
|
| 57 |
+
sliders = config_values.get('sliders', [])
|
| 58 |
+
preprocessor_sliders_config[config_name] = sliders
|
| 59 |
+
|
| 60 |
+
model_free_preprocessors = ["reference_only", "reference_adain", "reference_adain+attn"]
|
| 61 |
+
flag_preprocessor_resolution = "Preprocessor Resolution"
|
| 62 |
+
|
| 63 |
+
def build_sliders(module, pp):
|
| 64 |
+
grs = []
|
| 65 |
+
if module not in preprocessor_sliders_config:
|
| 66 |
+
grs += [
|
| 67 |
+
gr.update(label=flag_preprocessor_resolution, value=512, minimum=64, maximum=2048, step=1, visible=not pp, interactive=not pp),
|
| 68 |
+
gr.update(visible=False, interactive=False),
|
| 69 |
+
gr.update(visible=False, interactive=False),
|
| 70 |
+
gr.update(visible=True)
|
| 71 |
+
]
|
| 72 |
+
else:
|
| 73 |
+
for slider_config in preprocessor_sliders_config[module]:
|
| 74 |
+
if isinstance(slider_config, dict):
|
| 75 |
+
visible = True
|
| 76 |
+
if slider_config['name'] == flag_preprocessor_resolution:
|
| 77 |
+
visible = not pp
|
| 78 |
+
grs.append(gr.update(
|
| 79 |
+
label=slider_config['name'],
|
| 80 |
+
value=slider_config['value'],
|
| 81 |
+
minimum=slider_config['min'],
|
| 82 |
+
maximum=slider_config['max'],
|
| 83 |
+
step=slider_config['step'] if 'step' in slider_config else 1,
|
| 84 |
+
visible=visible,
|
| 85 |
+
interactive=visible))
|
| 86 |
+
else:
|
| 87 |
+
grs.append(gr.update(visible=False, interactive=False))
|
| 88 |
+
while len(grs) < 3:
|
| 89 |
+
grs.append(gr.update(visible=False, interactive=False))
|
| 90 |
+
grs.append(gr.update(visible=True))
|
| 91 |
+
if module in model_free_preprocessors:
|
| 92 |
+
grs += [gr.update(visible=False, value='None'), gr.update(visible=False)]
|
| 93 |
+
else:
|
| 94 |
+
grs += [gr.update(visible=True), gr.update(visible=True)]
|
| 95 |
+
return grs
|
| 96 |
+
|
| 97 |
+
refresh_symbol = '\U0001f504' # 🔄
|
| 98 |
+
switch_values_symbol = '\U000021C5' # ⇅
|
| 99 |
+
model_dropdowns = []
|
| 100 |
+
infotext_fields = []
|
| 101 |
+
|
| 102 |
+
def create_model_in_tab_ui(cn_id):
|
| 103 |
+
with gr.Row():
|
| 104 |
+
enabled = gr.Checkbox(label="Enable", value=False, interactive=True)
|
| 105 |
+
pixel_perfect = gr.Checkbox(label="Pixel Perfect", value=False, visible=False, interactive=True)
|
| 106 |
+
low_vram = gr.Checkbox(label="Low VRAM", value=False, visible=False, interactive=True)
|
| 107 |
+
overwrite_frames = gr.Checkbox(label='Overwrite input frames', value=True, visible=False, interactive=True)
|
| 108 |
+
with gr.Row(visible=False) as mod_row:
|
| 109 |
+
module = gr.Dropdown(cn_preprocessors, label=f"Preprocessor", value="none", interactive=True)
|
| 110 |
+
model = gr.Dropdown(cn_models, label=f"Model", value="None", interactive=True)
|
| 111 |
+
refresh_models = ToolButton(value=refresh_symbol)
|
| 112 |
+
refresh_models.click(refresh_all_models, model, model)
|
| 113 |
+
with gr.Row(visible=False) as weight_row:
|
| 114 |
+
weight = gr.Textbox(label="Weight schedule", lines=1, value='0:(1)', interactive=True)
|
| 115 |
+
with gr.Row(visible=False) as start_cs_row:
|
| 116 |
+
guidance_start = gr.Textbox(label="Starting Control Step schedule", lines=1, value='0:(0.0)', interactive=True)
|
| 117 |
+
with gr.Row(visible=False) as end_cs_row:
|
| 118 |
+
guidance_end = gr.Textbox(label="Ending Control Step schedule", lines=1, value='0:(1.0)', interactive=True)
|
| 119 |
+
model_dropdowns.append(model)
|
| 120 |
+
with gr.Column(visible=False) as advanced_column:
|
| 121 |
+
processor_res = gr.Slider(label="Annotator resolution", value=64, minimum=64, maximum=2048, interactive=False)
|
| 122 |
+
threshold_a = gr.Slider(label="Threshold A", value=64, minimum=64, maximum=1024, interactive=False)
|
| 123 |
+
threshold_b = gr.Slider(label="Threshold B", value=64, minimum=64, maximum=1024, interactive=False)
|
| 124 |
+
with gr.Row(visible=False) as vid_path_row:
|
| 125 |
+
vid_path = gr.Textbox(value='', label="ControlNet Input Video/ Image Path", interactive=True)
|
| 126 |
+
with gr.Row(visible=False) as mask_vid_path_row: # invisible temporarily since 26-04-23 until masks are fixed
|
| 127 |
+
mask_vid_path = gr.Textbox(value='', label="ControlNet Mask Video/ Image Path (*NOT WORKING, kept in UI for CN's devs testing!*)", interactive=True)
|
| 128 |
+
with gr.Row(visible=False) as control_mode_row:
|
| 129 |
+
control_mode = gr.Radio(choices=["Balanced", "My prompt is more important", "ControlNet is more important"], value="Balanced", label="Control Mode", interactive=True)
|
| 130 |
+
with gr.Row(visible=False) as env_row:
|
| 131 |
+
resize_mode = gr.Radio(choices=["Outer Fit (Shrink to Fit)", "Inner Fit (Scale to Fit)", "Just Resize"], value="Inner Fit (Scale to Fit)", label="Resize Mode", interactive=True)
|
| 132 |
+
with gr.Row(visible=False) as control_loopback_row:
|
| 133 |
+
loopback_mode = gr.Checkbox(label="LoopBack mode", value=False, interactive=True)
|
| 134 |
+
hide_output_list = [pixel_perfect, low_vram, mod_row, module, weight_row, start_cs_row, end_cs_row, env_row, overwrite_frames, vid_path_row, control_mode_row, mask_vid_path_row,
|
| 135 |
+
control_loopback_row] # add mask_vid_path_row when masks are working again
|
| 136 |
+
for cn_output in hide_output_list:
|
| 137 |
+
enabled.change(fn=hide_ui_by_cn_status, inputs=enabled, outputs=cn_output)
|
| 138 |
+
module.change(build_sliders, inputs=[module, pixel_perfect], outputs=[processor_res, threshold_a, threshold_b, advanced_column, model, refresh_models])
|
| 139 |
+
# hide vid/image input fields
|
| 140 |
+
loopback_outs = [vid_path_row, mask_vid_path_row]
|
| 141 |
+
for loopback_output in loopback_outs:
|
| 142 |
+
loopback_mode.change(fn=hide_file_textboxes, inputs=loopback_mode, outputs=loopback_output)
|
| 143 |
+
# handle pixel perfect ui changes
|
| 144 |
+
pixel_perfect.change(build_sliders, inputs=[module, pixel_perfect], outputs=[processor_res, threshold_a, threshold_b, advanced_column, model, refresh_models])
|
| 145 |
+
infotext_fields.extend([
|
| 146 |
+
(module, f"ControlNet Preprocessor"),
|
| 147 |
+
(model, f"ControlNet Model"),
|
| 148 |
+
(weight, f"ControlNet Weight"),
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
return {key: value for key, value in locals().items() if key in [
|
| 152 |
+
"enabled", "pixel_perfect", "low_vram", "module", "model", "weight",
|
| 153 |
+
"guidance_start", "guidance_end", "processor_res", "threshold_a", "threshold_b", "resize_mode", "control_mode",
|
| 154 |
+
"overwrite_frames", "vid_path", "mask_vid_path", "loopback_mode"
|
| 155 |
+
]}
|
| 156 |
+
|
| 157 |
+
def refresh_all_models(*inputs):
|
| 158 |
+
cn_models = cnet.get_models(update=True)
|
| 159 |
+
dd = inputs[0]
|
| 160 |
+
selected = dd if dd in cn_models else "None"
|
| 161 |
+
return gr.Dropdown.update(value=selected, choices=cn_models)
|
| 162 |
+
|
| 163 |
+
with gr.TabItem('ControlNet'):
|
| 164 |
+
gr.HTML(controlnet_infotext())
|
| 165 |
+
with gr.Tabs():
|
| 166 |
+
model_params = {}
|
| 167 |
+
for i in range(1, num_of_models + 1):
|
| 168 |
+
with gr.Tab(f"CN Model {i}"):
|
| 169 |
+
model_params[i] = create_model_in_tab_ui(i)
|
| 170 |
+
|
| 171 |
+
for key, value in model_params[i].items():
|
| 172 |
+
locals()[f"cn_{i}_{key}"] = value
|
| 173 |
+
|
| 174 |
+
return locals()
|
| 175 |
+
|
| 176 |
+
def setup_controlnet_ui():
|
| 177 |
+
if not find_controlnet():
|
| 178 |
+
gr.HTML("""<a style='target='_blank' href='https://github.com/Mikubill/sd-webui-controlnet'>ControlNet not found. Please install it :)</a>""", elem_id='controlnet_not_found_html_msg')
|
| 179 |
+
return {}
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
return setup_controlnet_ui_raw()
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f"'ControlNet UI setup failed with error: '{e}'!")
|
| 185 |
+
gr.HTML(f"""
|
| 186 |
+
Failed to setup ControlNet UI, check the reason in your commandline log. Please, downgrade your CN extension to <a style='color:Orange;' target='_blank' href='https://github.com/Mikubill/sd-webui-controlnet/archive/c9340671d6d59e5a79fc404f78f747f969f87374.zip'>c9340671d6d59e5a79fc404f78f747f969f87374</a> or report the problem <a style='color:Orange;' target='_blank' href='https://github.com/Mikubill/sd-webui-controlnet/issues'>here</a>.
|
| 187 |
+
""", elem_id='controlnet_not_found_html_msg')
|
| 188 |
+
return {}
|
| 189 |
+
|
| 190 |
+
def controlnet_component_names():
|
| 191 |
+
if not find_controlnet():
|
| 192 |
+
return []
|
| 193 |
+
|
| 194 |
+
return [f'cn_{i}_{component}' for i in range(1, num_of_models + 1) for component in [
|
| 195 |
+
'overwrite_frames', 'vid_path', 'mask_vid_path', 'enabled',
|
| 196 |
+
'low_vram', 'pixel_perfect',
|
| 197 |
+
'module', 'model', 'weight', 'guidance_start', 'guidance_end',
|
| 198 |
+
'processor_res', 'threshold_a', 'threshold_b', 'resize_mode', 'control_mode', 'loopback_mode'
|
| 199 |
+
]]
|
| 200 |
+
|
| 201 |
+
def process_with_controlnet(p, args, anim_args, controlnet_args, root, parseq_adapter, is_img2img=True, frame_idx=0):
|
| 202 |
+
CnSchKeys = ControlNetKeys(anim_args, controlnet_args) if not parseq_adapter.use_parseq else parseq_adapter.cn_keys
|
| 203 |
+
|
| 204 |
+
def read_cn_data(cn_idx):
|
| 205 |
+
cn_mask_np, cn_image_np = None, None
|
| 206 |
+
# Loopback mode ENABLED:
|
| 207 |
+
if getattr(controlnet_args, f'cn_{cn_idx}_loopback_mode'):
|
| 208 |
+
# On very first frame, check if use init enabled, and if init image is provided
|
| 209 |
+
if frame_idx == 0 and args.use_init and args.init_image is not None:
|
| 210 |
+
cn_image_np = load_image(args.init_image)
|
| 211 |
+
# convert to uint8 for compatibility with CN
|
| 212 |
+
cn_image_np = np.array(cn_image_np).astype('uint8')
|
| 213 |
+
# Not first frame, use previous img (init_sample)
|
| 214 |
+
elif frame_idx > 0 and root.init_sample:
|
| 215 |
+
cn_image_np = np.array(root.init_sample).astype('uint8')
|
| 216 |
+
else: # loopback mode is DISABLED
|
| 217 |
+
cn_inputframes = os.path.join(args.outdir, f'controlnet_{cn_idx}_inputframes') # set input frames folder path
|
| 218 |
+
if os.path.exists(cn_inputframes):
|
| 219 |
+
if count_files_in_folder(cn_inputframes) == 1:
|
| 220 |
+
cn_frame_path = os.path.join(cn_inputframes, "000000000.jpg")
|
| 221 |
+
print(f'Reading ControlNet *static* base frame at {cn_frame_path}')
|
| 222 |
+
else:
|
| 223 |
+
cn_frame_path = os.path.join(cn_inputframes, f"{frame_idx:09}.jpg")
|
| 224 |
+
print(f'Reading ControlNet {cn_idx} base frame #{frame_idx} at {cn_frame_path}')
|
| 225 |
+
if os.path.exists(cn_frame_path):
|
| 226 |
+
cn_image_np = np.array(Image.open(cn_frame_path).convert("RGB")).astype('uint8')
|
| 227 |
+
cn_maskframes = os.path.join(args.outdir, f'controlnet_{cn_idx}_maskframes') # set mask frames folder path
|
| 228 |
+
if os.path.exists(cn_maskframes):
|
| 229 |
+
if count_files_in_folder(cn_maskframes) == 1:
|
| 230 |
+
cn_mask_frame_path = os.path.join(cn_inputframes, "000000000.jpg")
|
| 231 |
+
print(f'Reading ControlNet *static* mask frame at {cn_mask_frame_path}')
|
| 232 |
+
else:
|
| 233 |
+
cn_mask_frame_path = os.path.join(args.outdir, f'controlnet_{cn_idx}_maskframes', f"{frame_idx:09}.jpg")
|
| 234 |
+
print(f'Reading ControlNet {cn_idx} mask frame #{frame_idx} at {cn_mask_frame_path}')
|
| 235 |
+
if os.path.exists(cn_mask_frame_path):
|
| 236 |
+
cn_mask_np = np.array(Image.open(cn_mask_frame_path).convert("RGB")).astype('uint8')
|
| 237 |
+
|
| 238 |
+
return cn_mask_np, cn_image_np
|
| 239 |
+
|
| 240 |
+
cnet = find_controlnet()
|
| 241 |
+
cn_data = [read_cn_data(i) for i in range(1, num_of_models + 1)]
|
| 242 |
+
|
| 243 |
+
# Check if any loopback_mode is set to True
|
| 244 |
+
any_loopback_mode = any(getattr(controlnet_args, f'cn_{i}_loopback_mode') for i in range(1, num_of_models + 1))
|
| 245 |
+
|
| 246 |
+
cn_inputframes_list = [os.path.join(args.outdir, f'controlnet_{i}_inputframes') for i in range(1, num_of_models + 1)]
|
| 247 |
+
|
| 248 |
+
if not any(os.path.exists(cn_inputframes) for cn_inputframes in cn_inputframes_list) and not any_loopback_mode:
|
| 249 |
+
print(f'\033[33mNeither the base nor the masking frames for ControlNet were found. Using the regular pipeline\033[0m')
|
| 250 |
+
|
| 251 |
+
p.scripts = scripts.scripts_img2img if is_img2img else scripts.scripts_txt2img
|
| 252 |
+
|
| 253 |
+
def create_cnu_dict(cn_args, prefix, img_np, mask_np, frame_idx, CnSchKeys):
|
| 254 |
+
|
| 255 |
+
keys = [
|
| 256 |
+
"enabled", "module", "model", "weight", "resize_mode", "control_mode", "low_vram", "pixel_perfect",
|
| 257 |
+
"processor_res", "threshold_a", "threshold_b", "guidance_start", "guidance_end"
|
| 258 |
+
]
|
| 259 |
+
cnu = {k: getattr(cn_args, f"{prefix}_{k}") for k in keys}
|
| 260 |
+
model_num = int(prefix.split('_')[-1]) # Extract model number from prefix (e.g., "cn_1" -> 1)
|
| 261 |
+
if 1 <= model_num <= 5:
|
| 262 |
+
# if in loopmode and no init image (img_np, after processing in this case) provided, disable CN unit for the very first frame. Will be enabled in the next frame automatically
|
| 263 |
+
if getattr(cn_args, f"cn_{model_num}_loopback_mode") and frame_idx == 0 and img_np is None:
|
| 264 |
+
cnu['enabled'] = False
|
| 265 |
+
cnu['weight'] = getattr(CnSchKeys, f"cn_{model_num}_weight_schedule_series")[frame_idx]
|
| 266 |
+
cnu['guidance_start'] = getattr(CnSchKeys, f"cn_{model_num}_guidance_start_schedule_series")[frame_idx]
|
| 267 |
+
cnu['guidance_end'] = getattr(CnSchKeys, f"cn_{model_num}_guidance_end_schedule_series")[frame_idx]
|
| 268 |
+
if cnu['enabled']:
|
| 269 |
+
debug_print(f"ControlNet {model_num}: weight={cnu['weight']}, guidance_start={cnu['guidance_start']}, guidance_end={cnu['guidance_end']}")
|
| 270 |
+
cnu['image'] = {'image': img_np, 'mask': mask_np} if mask_np is not None else img_np
|
| 271 |
+
|
| 272 |
+
return cnu
|
| 273 |
+
|
| 274 |
+
masks_np, images_np = zip(*cn_data)
|
| 275 |
+
|
| 276 |
+
cn_units = [cnet.ControlNetUnit(**create_cnu_dict(controlnet_args, f"cn_{i + 1}", img_np, mask_np, frame_idx, CnSchKeys))
|
| 277 |
+
for i, (img_np, mask_np) in enumerate(zip(images_np, masks_np))]
|
| 278 |
+
|
| 279 |
+
p.script_args = {"enabled": True}
|
| 280 |
+
cnet.update_cn_script_in_processing(p, cn_units, is_img2img=is_img2img, is_ui=False)
|
| 281 |
+
|
| 282 |
+
def process_controlnet_input_frames(args, anim_args, controlnet_args, video_path, mask_path, outdir_suffix, id):
|
| 283 |
+
if (video_path or mask_path) and getattr(controlnet_args, f'cn_{id}_enabled'):
|
| 284 |
+
frame_path = os.path.join(args.outdir, f'controlnet_{id}_{outdir_suffix}')
|
| 285 |
+
os.makedirs(frame_path, exist_ok=True)
|
| 286 |
+
|
| 287 |
+
accepted_image_extensions = ('.jpg', '.jpeg', '.png', '.bmp')
|
| 288 |
+
if video_path and video_path.lower().endswith(accepted_image_extensions):
|
| 289 |
+
convert_image(video_path, os.path.join(frame_path, '000000000.jpg'))
|
| 290 |
+
print(f"Copied CN Model {id}'s single input image to inputframes folder!")
|
| 291 |
+
elif mask_path and mask_path.lower().endswith(accepted_image_extensions):
|
| 292 |
+
convert_image(mask_path, os.path.join(frame_path, '000000000.jpg'))
|
| 293 |
+
print(f"Copied CN Model {id}'s single input image to inputframes *mask* folder!")
|
| 294 |
+
else:
|
| 295 |
+
print(f'Unpacking ControlNet {id} {"video mask" if mask_path else "base video"}')
|
| 296 |
+
print(f"Exporting Video Frames to {frame_path}...")
|
| 297 |
+
vid2frames(
|
| 298 |
+
video_path=video_path or mask_path,
|
| 299 |
+
video_in_frame_path=frame_path,
|
| 300 |
+
n=1 if anim_args.animation_mode != 'Video Input' else anim_args.extract_nth_frame,
|
| 301 |
+
overwrite=getattr(controlnet_args, f'cn_{id}_overwrite_frames'),
|
| 302 |
+
extract_from_frame=0 if anim_args.animation_mode != 'Video Input' else anim_args.extract_from_frame,
|
| 303 |
+
extract_to_frame=(anim_args.max_frames - 1) if anim_args.animation_mode != 'Video Input' else anim_args.extract_to_frame,
|
| 304 |
+
numeric_files_output=True
|
| 305 |
+
)
|
| 306 |
+
print(f"Loading {anim_args.max_frames} input frames from {frame_path} and saving video frames to {args.outdir}")
|
| 307 |
+
print(f'ControlNet {id} {"video mask" if mask_path else "base video"} unpacked!')
|
| 308 |
+
|
| 309 |
+
def unpack_controlnet_vids(args, anim_args, controlnet_args):
|
| 310 |
+
# this func gets called from render.py once for an entire animation run -->
|
| 311 |
+
# tries to trigger an extraction of CN input frames (regular + masks) from video or image
|
| 312 |
+
for i in range(1, num_of_models + 1):
|
| 313 |
+
# LoopBack mode is enabled, no need to extract a video or copy an init image
|
| 314 |
+
if getattr(controlnet_args, f'cn_{i}_loopback_mode'):
|
| 315 |
+
print(f"ControlNet #{i} is in LoopBack mode, skipping video/ image extraction stage.")
|
| 316 |
+
continue
|
| 317 |
+
vid_path = clean_gradio_path_strings(getattr(controlnet_args, f'cn_{i}_vid_path', None))
|
| 318 |
+
mask_path = clean_gradio_path_strings(getattr(controlnet_args, f'cn_{i}_mask_vid_path', None))
|
| 319 |
+
|
| 320 |
+
if vid_path: # Process base video, if available
|
| 321 |
+
process_controlnet_input_frames(args, anim_args, controlnet_args, vid_path, None, 'inputframes', i)
|
| 322 |
+
|
| 323 |
+
if mask_path: # Process mask video, if available
|
| 324 |
+
process_controlnet_input_frames(args, anim_args, controlnet_args, None, mask_path, 'maskframes', i)
|
scripts/deforum_helpers/deforum_controlnet_gradio.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
# print (cnet_1.get_modules())
|
| 3 |
+
|
| 4 |
+
# *** TODO: re-enable table printing! disabled only temp! 13-04-23 ***
|
| 5 |
+
# table = Table(title="ControlNet params",padding=0, box=box.ROUNDED)
|
| 6 |
+
|
| 7 |
+
# TODO: auto infer the names and the values for the table
|
| 8 |
+
# field_names = []
|
| 9 |
+
# field_names += ["module", "model", "weight", "inv", "guide_start", "guide_end", "guess", "resize", "rgb_bgr", "proc res", "thr a", "thr b"]
|
| 10 |
+
# for field_name in field_names:
|
| 11 |
+
# table.add_column(field_name, justify="center")
|
| 12 |
+
|
| 13 |
+
# cn_model_name = str(controlnet_args.cn_1_model)
|
| 14 |
+
|
| 15 |
+
# rows = []
|
| 16 |
+
# rows += [controlnet_args.cn_1_module, cn_model_name[len('control_'):] if 'control_' in cn_model_name else cn_model_name, controlnet_args.cn_1_weight, controlnet_args.cn_1_invert_image, controlnet_args.cn_1_guidance_start, controlnet_args.cn_1_guidance_end, controlnet_args.cn_1_guess_mode, controlnet_args.cn_1_resize_mode, controlnet_args.cn_1_rgbbgr_mode, controlnet_args.cn_1_processor_res, controlnet_args.cn_1_threshold_a, controlnet_args.cn_1_threshold_b]
|
| 17 |
+
# rows = [str(x) for x in rows]
|
| 18 |
+
|
| 19 |
+
# table.add_row(*rows)
|
| 20 |
+
# console.print(table)
|
| 21 |
+
|
| 22 |
+
def hide_ui_by_cn_status(choice):
|
| 23 |
+
return gr.update(visible=True) if choice else gr.update(visible=False)
|
| 24 |
+
|
| 25 |
+
def hide_file_textboxes(choice):
|
| 26 |
+
return gr.update(visible=False) if choice else gr.update(visible=True)
|
| 27 |
+
|
| 28 |
+
class ToolButton(gr.Button, gr.components.FormComponent):
|
| 29 |
+
"""Small button with single emoji as text, fits inside gradio forms"""
|
| 30 |
+
def __init__(self, **kwargs):
|
| 31 |
+
super().__init__(variant="tool", **kwargs)
|
| 32 |
+
|
| 33 |
+
def get_block_name(self):
|
| 34 |
+
return "button"
|
scripts/deforum_helpers/deforum_tqdm.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from math import ceil
|
| 3 |
+
import tqdm
|
| 4 |
+
from modules.shared import progress_print_out, opts, cmd_opts
|
| 5 |
+
|
| 6 |
+
class DeforumTQDM:
|
| 7 |
+
def __init__(self, args, anim_args, parseq_args, video_args):
|
| 8 |
+
self._tqdm = None
|
| 9 |
+
self._args = args
|
| 10 |
+
self._anim_args = anim_args
|
| 11 |
+
self._parseq_args = parseq_args
|
| 12 |
+
self._video_args = video_args
|
| 13 |
+
|
| 14 |
+
def reset(self):
|
| 15 |
+
from .animation_key_frames import DeformAnimKeys
|
| 16 |
+
from .parseq_adapter import ParseqAdapter
|
| 17 |
+
deforum_total = 0
|
| 18 |
+
# FIXME: get only amount of steps
|
| 19 |
+
parseq_adapter = ParseqAdapter(self._parseq_args, self._anim_args, self._video_args, None, mute=True)
|
| 20 |
+
keys = DeformAnimKeys(self._anim_args) if not parseq_adapter.use_parseq else parseq_adapter.anim_keys
|
| 21 |
+
|
| 22 |
+
start_frame = 0
|
| 23 |
+
if self._anim_args.resume_from_timestring:
|
| 24 |
+
for tmp in os.listdir(self._args.outdir):
|
| 25 |
+
filename = tmp.split("_")
|
| 26 |
+
# don't use saved depth maps to count number of frames
|
| 27 |
+
if self._anim_args.resume_timestring in filename and "depth" not in filename:
|
| 28 |
+
start_frame += 1
|
| 29 |
+
start_frame = start_frame - 1
|
| 30 |
+
using_vid_init = self._anim_args.animation_mode == 'Video Input'
|
| 31 |
+
turbo_steps = 1 if using_vid_init else int(self._anim_args.diffusion_cadence)
|
| 32 |
+
if self._anim_args.resume_from_timestring:
|
| 33 |
+
last_frame = start_frame - 1
|
| 34 |
+
if turbo_steps > 1:
|
| 35 |
+
last_frame -= last_frame % turbo_steps
|
| 36 |
+
if turbo_steps > 1:
|
| 37 |
+
turbo_next_frame_idx = last_frame
|
| 38 |
+
turbo_prev_frame_idx = turbo_next_frame_idx
|
| 39 |
+
start_frame = last_frame + turbo_steps
|
| 40 |
+
frame_idx = start_frame
|
| 41 |
+
had_first = False
|
| 42 |
+
while frame_idx < self._anim_args.max_frames:
|
| 43 |
+
strength = keys.strength_schedule_series[frame_idx]
|
| 44 |
+
if not had_first and self._args.use_init and self._args.init_image is not None and self._args.init_image != '':
|
| 45 |
+
deforum_total += int(ceil(self._args.steps * (1 - strength)))
|
| 46 |
+
had_first = True
|
| 47 |
+
elif not had_first:
|
| 48 |
+
deforum_total += self._args.steps
|
| 49 |
+
had_first = True
|
| 50 |
+
else:
|
| 51 |
+
deforum_total += int(ceil(self._args.steps * (1 - strength)))
|
| 52 |
+
|
| 53 |
+
if turbo_steps > 1:
|
| 54 |
+
frame_idx += turbo_steps
|
| 55 |
+
else:
|
| 56 |
+
frame_idx += 1
|
| 57 |
+
|
| 58 |
+
self._tqdm = tqdm.tqdm(
|
| 59 |
+
desc="Deforum progress",
|
| 60 |
+
total=deforum_total,
|
| 61 |
+
position=1,
|
| 62 |
+
file=progress_print_out
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def update(self):
|
| 66 |
+
if not opts.multiple_tqdm or cmd_opts.disable_console_progressbars:
|
| 67 |
+
return
|
| 68 |
+
if self._tqdm is None:
|
| 69 |
+
self.reset()
|
| 70 |
+
self._tqdm.update()
|
| 71 |
+
|
| 72 |
+
def updateTotal(self, new_total):
|
| 73 |
+
if not opts.multiple_tqdm or cmd_opts.disable_console_progressbars:
|
| 74 |
+
return
|
| 75 |
+
if self._tqdm is None:
|
| 76 |
+
self.reset()
|
| 77 |
+
self._tqdm.total = new_total
|
| 78 |
+
|
| 79 |
+
def clear(self):
|
| 80 |
+
if self._tqdm is not None:
|
| 81 |
+
self._tqdm.close()
|
| 82 |
+
self._tqdm = None
|
scripts/deforum_helpers/deprecation_utils.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is used to map deprecated setting names in a dictionary
|
| 2 |
+
# and print a message containing the old and the new names
|
| 3 |
+
|
| 4 |
+
deprecation_map = {
|
| 5 |
+
"histogram_matching": None,
|
| 6 |
+
"flip_2d_perspective": "enable_perspective_flip",
|
| 7 |
+
"skip_video_for_run_all": "skip_video_creation",
|
| 8 |
+
"color_coherence": [
|
| 9 |
+
("Match Frame 0 HSV", "HSV", False),
|
| 10 |
+
("Match Frame 0 LAB", "LAB", False),
|
| 11 |
+
("Match Frame 0 RGB", "RGB", False),
|
| 12 |
+
# ,("removed_value", None, True) # for removed values, if we'll need in the future
|
| 13 |
+
],
|
| 14 |
+
"hybrid_composite": [
|
| 15 |
+
(False, "None", False),
|
| 16 |
+
(True, "Normal", False),
|
| 17 |
+
],
|
| 18 |
+
"optical_flow_redo_generation": [
|
| 19 |
+
(False, "None", False),
|
| 20 |
+
(True, "DIS Fine", False),
|
| 21 |
+
],
|
| 22 |
+
"optical_flow_cadence": [
|
| 23 |
+
(False, "None", False),
|
| 24 |
+
(True, "DIS Fine", False),
|
| 25 |
+
],
|
| 26 |
+
"cn_1_resize_mode": [
|
| 27 |
+
("Envelope (Outer Fit)", "Outer Fit (Shrink to Fit)", False),
|
| 28 |
+
("Scale to Fit (Inner Fit)", "Inner Fit (Scale to Fit)", False),
|
| 29 |
+
],
|
| 30 |
+
"cn_2_resize_mode": [
|
| 31 |
+
("Envelope (Outer Fit)", "Outer Fit (Shrink to Fit)", False),
|
| 32 |
+
("Scale to Fit (Inner Fit)", "Inner Fit (Scale to Fit)", False),
|
| 33 |
+
],
|
| 34 |
+
"cn_3_resize_mode": [
|
| 35 |
+
("Envelope (Outer Fit)", "Outer Fit (Shrink to Fit)", False),
|
| 36 |
+
("Scale to Fit (Inner Fit)", "Inner Fit (Scale to Fit)", False),
|
| 37 |
+
],
|
| 38 |
+
"use_zoe_depth": ("depth_algorithm", [("True", "Zoe+AdaBins (old)"), ("False", "Midas+AdaBins (old)")]),
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def dynamic_num_to_schedule_formatter(old_value):
|
| 42 |
+
return f"0:({old_value})"
|
| 43 |
+
|
| 44 |
+
for i in range(1, 6): # 5 CN models in total
|
| 45 |
+
deprecation_map[f"cn_{i}_weight"] = dynamic_num_to_schedule_formatter
|
| 46 |
+
deprecation_map[f"cn_{i}_guidance_start"] = dynamic_num_to_schedule_formatter
|
| 47 |
+
deprecation_map[f"cn_{i}_guidance_end"] = dynamic_num_to_schedule_formatter
|
| 48 |
+
|
| 49 |
+
def handle_deprecated_settings(settings_json):
|
| 50 |
+
# Set legacy_colormatch mode to True when importing old files, so results are backwards-compatible. Print a message about it too
|
| 51 |
+
if 'legacy_colormatch' not in settings_json:
|
| 52 |
+
settings_json['legacy_colormatch'] = True
|
| 53 |
+
print('\033[33mlegacy_colormatch is missing from settings file, so we are setting it to *True* for backwards compatability. You are welcome to test your file with that setting being disabled for better color coherency.\033[0m')
|
| 54 |
+
print("")
|
| 55 |
+
for setting_name, deprecation_info in deprecation_map.items():
|
| 56 |
+
if setting_name in settings_json:
|
| 57 |
+
if deprecation_info is None:
|
| 58 |
+
print(f"WARNING: Setting '{setting_name}' has been removed. It will be discarded and the default value used instead!")
|
| 59 |
+
elif isinstance(deprecation_info, tuple):
|
| 60 |
+
new_setting_name, value_map = deprecation_info
|
| 61 |
+
old_value = str(settings_json.pop(setting_name)) # Convert the boolean value to a string for comparison
|
| 62 |
+
new_value = next((v for k, v in value_map if k == old_value), None)
|
| 63 |
+
if new_value is not None:
|
| 64 |
+
print(f"WARNING: Setting '{setting_name}' has been renamed to '{new_setting_name}' with value '{new_value}'. The saved settings file will reflect the change")
|
| 65 |
+
settings_json[new_setting_name] = new_value
|
| 66 |
+
elif callable(deprecation_info):
|
| 67 |
+
old_value = settings_json[setting_name]
|
| 68 |
+
if isinstance(old_value, (int, float)):
|
| 69 |
+
new_value = deprecation_info(old_value)
|
| 70 |
+
print(f"WARNING: Value '{old_value}' for setting '{setting_name}' has been replaced with '{new_value}'. The saved settings file will reflect the change")
|
| 71 |
+
settings_json[setting_name] = new_value
|
| 72 |
+
elif isinstance(deprecation_info, str):
|
| 73 |
+
print(f"WARNING: Setting '{setting_name}' has been renamed to '{deprecation_info}'. The saved settings file will reflect the change")
|
| 74 |
+
settings_json[deprecation_info] = settings_json.pop(setting_name)
|
| 75 |
+
elif isinstance(deprecation_info, list):
|
| 76 |
+
for old_value, new_value, is_removed in deprecation_info:
|
| 77 |
+
if settings_json[setting_name] == old_value:
|
| 78 |
+
if is_removed:
|
| 79 |
+
print(f"WARNING: Value '{old_value}' for setting '{setting_name}' has been removed. It will be discarded and the default value used instead!")
|
| 80 |
+
else:
|
| 81 |
+
print(f"WARNING: Value '{old_value}' for setting '{setting_name}' has been replaced with '{new_value}'. The saved settings file will reflect the change")
|
| 82 |
+
settings_json[setting_name] = new_value
|
scripts/deforum_helpers/depth.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from einops import rearrange, repeat
|
| 7 |
+
from modules import devices
|
| 8 |
+
from modules.shared import cmd_opts
|
| 9 |
+
from .depth_adabins import AdaBinsModel
|
| 10 |
+
from .depth_leres import LeReSDepth
|
| 11 |
+
from .depth_midas import MidasDepth
|
| 12 |
+
from .depth_zoe import ZoeDepth
|
| 13 |
+
from .general_utils import debug_print
|
| 14 |
+
|
| 15 |
+
class DepthModel:
|
| 16 |
+
_instance = None
|
| 17 |
+
|
| 18 |
+
def __new__(cls, *args, **kwargs):
|
| 19 |
+
keep_in_vram = kwargs.get('keep_in_vram', False)
|
| 20 |
+
depth_algorithm = kwargs.get('depth_algorithm', 'Midas-3-Hybrid')
|
| 21 |
+
Width, Height = kwargs.get('Width', 512), kwargs.get('Height', 512)
|
| 22 |
+
midas_weight = kwargs.get('midas_weight', 0.2)
|
| 23 |
+
model_switched = cls._instance and cls._instance.depth_algorithm != depth_algorithm
|
| 24 |
+
resolution_changed = cls._instance and (cls._instance.Width != Width or cls._instance.Height != Height)
|
| 25 |
+
zoe_algorithm = 'zoe' in depth_algorithm.lower()
|
| 26 |
+
model_deleted = cls._instance and cls._instance.should_delete
|
| 27 |
+
|
| 28 |
+
should_reload = (cls._instance is None or model_deleted or model_switched or (zoe_algorithm and resolution_changed))
|
| 29 |
+
|
| 30 |
+
if should_reload:
|
| 31 |
+
cls._instance = super().__new__(cls)
|
| 32 |
+
cls._instance._initialize(models_path=args[0], device=args[1], half_precision=not cmd_opts.no_half, keep_in_vram=keep_in_vram, depth_algorithm=depth_algorithm, Width=Width, Height=Height, midas_weight=midas_weight)
|
| 33 |
+
elif cls._instance.should_delete and keep_in_vram:
|
| 34 |
+
cls._instance._initialize(models_path=args[0], device=args[1], half_precision=not cmd_opts.no_half, keep_in_vram=keep_in_vram, depth_algorithm=depth_algorithm, Width=Width, Height=Height, midas_weight=midas_weight)
|
| 35 |
+
cls._instance.should_delete = not keep_in_vram
|
| 36 |
+
return cls._instance
|
| 37 |
+
|
| 38 |
+
def _initialize(self, models_path, device, half_precision=not cmd_opts.no_half, keep_in_vram=False, depth_algorithm='Midas-3-Hybrid', Width=512, Height=512, midas_weight=1.0):
|
| 39 |
+
self.models_path = models_path
|
| 40 |
+
self.device = device
|
| 41 |
+
self.half_precision = half_precision
|
| 42 |
+
self.keep_in_vram = keep_in_vram
|
| 43 |
+
self.depth_algorithm = depth_algorithm
|
| 44 |
+
self.Width, self.Height = Width, Height
|
| 45 |
+
self.midas_weight = midas_weight
|
| 46 |
+
self.depth_min, self.depth_max = 1000, -1000
|
| 47 |
+
self.adabins_helper = None
|
| 48 |
+
self._initialize_model()
|
| 49 |
+
|
| 50 |
+
def _initialize_model(self):
|
| 51 |
+
depth_algo = self.depth_algorithm.lower()
|
| 52 |
+
if depth_algo.startswith('zoe'):
|
| 53 |
+
self.zoe_depth = ZoeDepth(self.Width, self.Height)
|
| 54 |
+
if depth_algo == 'zoe+adabins (old)':
|
| 55 |
+
self.adabins_model = AdaBinsModel(self.models_path, keep_in_vram=self.keep_in_vram)
|
| 56 |
+
self.adabins_helper = self.adabins_model.adabins_helper
|
| 57 |
+
elif depth_algo == 'leres':
|
| 58 |
+
self.leres_depth = LeReSDepth(width=448, height=448, models_path=self.models_path, checkpoint_name='res101.pth', backbone='resnext101')
|
| 59 |
+
elif depth_algo == 'adabins':
|
| 60 |
+
self.adabins_model = AdaBinsModel(self.models_path, keep_in_vram=self.keep_in_vram)
|
| 61 |
+
self.adabins_helper = self.adabins_model.adabins_helper
|
| 62 |
+
elif depth_algo.startswith('midas'):
|
| 63 |
+
self.midas_depth = MidasDepth(self.models_path, self.device, half_precision=self.half_precision, midas_model_type=self.depth_algorithm)
|
| 64 |
+
if depth_algo == 'midas+adabins (old)':
|
| 65 |
+
self.adabins_model = AdaBinsModel(self.models_path, keep_in_vram=self.keep_in_vram)
|
| 66 |
+
self.adabins_helper = self.adabins_model.adabins_helper
|
| 67 |
+
else:
|
| 68 |
+
raise Exception(f"Unknown depth_algorithm: {self.depth_algorithm}")
|
| 69 |
+
|
| 70 |
+
def predict(self, prev_img_cv2, midas_weight, half_precision) -> torch.Tensor:
|
| 71 |
+
|
| 72 |
+
img_pil = Image.fromarray(cv2.cvtColor(prev_img_cv2.astype(np.uint8), cv2.COLOR_RGB2BGR))
|
| 73 |
+
|
| 74 |
+
if self.depth_algorithm.lower().startswith('zoe'):
|
| 75 |
+
depth_tensor = self.zoe_depth.predict(img_pil).to(self.device)
|
| 76 |
+
if self.depth_algorithm.lower() == 'zoe+adabins (old)' and midas_weight < 1.0:
|
| 77 |
+
use_adabins, adabins_depth = AdaBinsModel._instance.predict(img_pil, prev_img_cv2)
|
| 78 |
+
if use_adabins: # if there was no error in getting the adabins depth, align midas with adabins
|
| 79 |
+
depth_tensor = self.blend_and_align_with_adabins(depth_tensor, adabins_depth, midas_weight)
|
| 80 |
+
elif self.depth_algorithm.lower() == 'leres':
|
| 81 |
+
depth_tensor = self.leres_depth.predict(prev_img_cv2.astype(np.float32) / 255.0)
|
| 82 |
+
elif self.depth_algorithm.lower() == 'adabins':
|
| 83 |
+
use_adabins, adabins_depth = AdaBinsModel._instance.predict(img_pil, prev_img_cv2)
|
| 84 |
+
depth_tensor = torch.tensor(adabins_depth)
|
| 85 |
+
if use_adabins is False:
|
| 86 |
+
raise Exception("Error getting depth from AdaBins") # TODO: fallback to something else maybe?
|
| 87 |
+
elif self.depth_algorithm.lower().startswith('midas'):
|
| 88 |
+
depth_tensor = self.midas_depth.predict(prev_img_cv2, half_precision)
|
| 89 |
+
if self.depth_algorithm.lower() == 'midas+adabins (old)' and midas_weight < 1.0:
|
| 90 |
+
use_adabins, adabins_depth = AdaBinsModel._instance.predict(img_pil, prev_img_cv2)
|
| 91 |
+
if use_adabins: # if there was no error in getting the adabins depth, align midas with adabins
|
| 92 |
+
depth_tensor = self.blend_and_align_with_adabins(depth_tensor, adabins_depth, midas_weight)
|
| 93 |
+
else: # Unknown!
|
| 94 |
+
raise Exception(f"Unknown depth_algorithm passed to depth.predict function: {self.depth_algorithm}")
|
| 95 |
+
|
| 96 |
+
return depth_tensor
|
| 97 |
+
|
| 98 |
+
def blend_and_align_with_adabins(self, depth_tensor, adabins_depth, midas_weight):
|
| 99 |
+
depth_tensor = torch.subtract(50.0, depth_tensor) / 19.0 # align midas depth with adabins depth. Original alignment code from Disco Diffusion
|
| 100 |
+
blended_depth_map = (depth_tensor.cpu().numpy() * midas_weight + adabins_depth * (1.0 - midas_weight))
|
| 101 |
+
depth_tensor = torch.from_numpy(np.expand_dims(blended_depth_map, axis=0)).squeeze().to(self.device)
|
| 102 |
+
debug_print(f"Blended Midas Depth with AdaBins Depth")
|
| 103 |
+
return depth_tensor
|
| 104 |
+
|
| 105 |
+
def to(self, device):
|
| 106 |
+
self.device = device
|
| 107 |
+
if self.depth_algorithm.lower().startswith('zoe'):
|
| 108 |
+
self.zoe_depth.zoe.to(device)
|
| 109 |
+
elif self.depth_algorithm.lower() == 'leres':
|
| 110 |
+
self.leres_depth.to(device)
|
| 111 |
+
elif self.depth_algorithm.lower().startswith('midas'):
|
| 112 |
+
self.midas_depth.to(device)
|
| 113 |
+
if hasattr(self, 'adabins_model'):
|
| 114 |
+
self.adabins_model.to(device)
|
| 115 |
+
gc.collect()
|
| 116 |
+
torch.cuda.empty_cache()
|
| 117 |
+
|
| 118 |
+
def to_image(self, depth: torch.Tensor):
|
| 119 |
+
depth = depth.cpu().numpy()
|
| 120 |
+
depth = np.expand_dims(depth, axis=0) if len(depth.shape) == 2 else depth
|
| 121 |
+
self.depth_min, self.depth_max = min(self.depth_min, depth.min()), max(self.depth_max, depth.max())
|
| 122 |
+
denom = max(1e-8, self.depth_max - self.depth_min)
|
| 123 |
+
temp = rearrange((depth - self.depth_min) / denom * 255, 'c h w -> h w c')
|
| 124 |
+
return Image.fromarray(repeat(temp, 'h w 1 -> h w c', c=3).astype(np.uint8))
|
| 125 |
+
|
| 126 |
+
def save(self, filename: str, depth: torch.Tensor):
|
| 127 |
+
self.to_image(depth).save(filename)
|
| 128 |
+
|
| 129 |
+
def delete_model(self):
|
| 130 |
+
for attr in ['zoe_depth', 'leres_depth']:
|
| 131 |
+
if hasattr(self, attr):
|
| 132 |
+
getattr(self, attr).delete()
|
| 133 |
+
delattr(self, attr)
|
| 134 |
+
|
| 135 |
+
if hasattr(self, 'midas_depth'):
|
| 136 |
+
del self.midas_depth
|
| 137 |
+
|
| 138 |
+
if hasattr(self, 'adabins_model'):
|
| 139 |
+
self.adabins_model.delete_model()
|
| 140 |
+
|
| 141 |
+
gc.collect()
|
| 142 |
+
torch.cuda.empty_cache()
|
| 143 |
+
devices.torch_gc()
|
scripts/deforum_helpers/depth_adabins.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import torchvision.transforms.functional as TF
|
| 5 |
+
from .general_utils import download_file_with_checksum
|
| 6 |
+
from infer import InferenceHelper
|
| 7 |
+
|
| 8 |
+
class AdaBinsModel:
|
| 9 |
+
_instance = None
|
| 10 |
+
|
| 11 |
+
def __new__(cls, *args, **kwargs):
|
| 12 |
+
keep_in_vram = kwargs.get('keep_in_vram', False)
|
| 13 |
+
if cls._instance is None:
|
| 14 |
+
cls._instance = super().__new__(cls)
|
| 15 |
+
cls._instance._initialize(*args, keep_in_vram=keep_in_vram)
|
| 16 |
+
return cls._instance
|
| 17 |
+
|
| 18 |
+
def _initialize(self, models_path, keep_in_vram=False):
|
| 19 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 20 |
+
self.keep_in_vram = keep_in_vram
|
| 21 |
+
self.adabins_helper = None
|
| 22 |
+
|
| 23 |
+
download_file_with_checksum(url='https://github.com/hithereai/deforum-for-automatic1111-webui/releases/download/AdaBins/AdaBins_nyu.pt', expected_checksum='643db9785c663aca72f66739427642726b03acc6c4c1d3755a4587aa2239962746410d63722d87b49fc73581dbc98ed8e3f7e996ff7b9c0d56d0fbc98e23e41a', dest_folder=models_path, dest_filename='AdaBins_nyu.pt')
|
| 24 |
+
|
| 25 |
+
self.adabins_helper = InferenceHelper(models_path=models_path, dataset='nyu', device=self.device)
|
| 26 |
+
|
| 27 |
+
def predict(self, img_pil, prev_img_cv2):
|
| 28 |
+
w, h = prev_img_cv2.shape[1], prev_img_cv2.shape[0]
|
| 29 |
+
adabins_depth = np.array([])
|
| 30 |
+
use_adabins = True
|
| 31 |
+
MAX_ADABINS_AREA, MIN_ADABINS_AREA = 500000, 448 * 448
|
| 32 |
+
|
| 33 |
+
image_pil_area, resized = w * h, False
|
| 34 |
+
|
| 35 |
+
if image_pil_area not in range(MIN_ADABINS_AREA, MAX_ADABINS_AREA + 1):
|
| 36 |
+
scale = ((MAX_ADABINS_AREA if image_pil_area > MAX_ADABINS_AREA else MIN_ADABINS_AREA) / image_pil_area) ** 0.5
|
| 37 |
+
depth_input = img_pil.resize((int(w * scale), int(h * scale)), Image.LANCZOS if image_pil_area > MAX_ADABINS_AREA else Image.BICUBIC)
|
| 38 |
+
print(f"AdaBins depth resized to {depth_input.width}x{depth_input.height}")
|
| 39 |
+
resized = True
|
| 40 |
+
else:
|
| 41 |
+
depth_input = img_pil
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
with torch.no_grad():
|
| 45 |
+
_, adabins_depth = self.adabins_helper.predict_pil(depth_input)
|
| 46 |
+
if resized:
|
| 47 |
+
adabins_depth = TF.resize(torch.from_numpy(adabins_depth), torch.Size([h, w]), interpolation=TF.InterpolationMode.BICUBIC).cpu().numpy()
|
| 48 |
+
adabins_depth = adabins_depth.squeeze()
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print("AdaBins exception encountered. Falling back to pure MiDaS/Zoe (only if running in Legacy Midas/Zoe+AdaBins mode)")
|
| 51 |
+
use_adabins = False
|
| 52 |
+
torch.cuda.empty_cache()
|
| 53 |
+
|
| 54 |
+
return use_adabins, adabins_depth
|
| 55 |
+
|
| 56 |
+
def to(self, device):
|
| 57 |
+
self.device = device
|
| 58 |
+
if self.adabins_helper is not None:
|
| 59 |
+
self.adabins_helper.to(device)
|
| 60 |
+
|
| 61 |
+
def delete_model(self):
|
| 62 |
+
del self.adabins_helper
|
scripts/deforum_helpers/depth_leres.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import cv2
|
| 3 |
+
import os
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torchvision.transforms as transforms
|
| 6 |
+
from .general_utils import download_file_with_checksum
|
| 7 |
+
from leres.lib.multi_depth_model_woauxi import RelDepthModel
|
| 8 |
+
from leres.lib.net_tools import load_ckpt
|
| 9 |
+
|
| 10 |
+
class LeReSDepth:
|
| 11 |
+
def __init__(self, width=448, height=448, models_path=None, checkpoint_name='res101.pth', backbone='resnext101'):
|
| 12 |
+
self.width = width
|
| 13 |
+
self.height = height
|
| 14 |
+
self.models_path = models_path
|
| 15 |
+
self.checkpoint_name = checkpoint_name
|
| 16 |
+
self.backbone = backbone
|
| 17 |
+
|
| 18 |
+
download_file_with_checksum(url='https://cloudstor.aarnet.edu.au/plus/s/lTIJF4vrvHCAI31/download', expected_checksum='7fdc870ae6568cb28d56700d0be8fc45541e09cea7c4f84f01ab47de434cfb7463cacae699ad19fe40ee921849f9760dedf5e0dec04a62db94e169cf203f55b1', dest_folder=models_path, dest_filename=self.checkpoint_name)
|
| 19 |
+
|
| 20 |
+
self.depth_model = RelDepthModel(backbone=self.backbone)
|
| 21 |
+
self.depth_model.eval()
|
| 22 |
+
self.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 23 |
+
self.depth_model.to(self.DEVICE)
|
| 24 |
+
load_ckpt(os.path.join(self.models_path, self.checkpoint_name), self.depth_model, None, None)
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def scale_torch(img):
|
| 28 |
+
if len(img.shape) == 2:
|
| 29 |
+
img = img[np.newaxis, :, :]
|
| 30 |
+
if img.shape[2] == 3:
|
| 31 |
+
transform = transforms.Compose([transforms.ToTensor(),
|
| 32 |
+
transforms.Normalize((0.485, 0.456, 0.406) , (0.229, 0.224, 0.225))])
|
| 33 |
+
img = transform(img)
|
| 34 |
+
else:
|
| 35 |
+
img = img.astype(np.float32)
|
| 36 |
+
img = torch.from_numpy(img)
|
| 37 |
+
return img
|
| 38 |
+
|
| 39 |
+
def predict(self, image):
|
| 40 |
+
resized_image = cv2.resize(image, (self.width, self.height))
|
| 41 |
+
img_torch = self.scale_torch(resized_image)[None, :, :, :]
|
| 42 |
+
pred_depth = self.depth_model.inference(img_torch).cpu().numpy().squeeze()
|
| 43 |
+
pred_depth_ori = cv2.resize(pred_depth, (image.shape[1], image.shape[0]))
|
| 44 |
+
return torch.from_numpy(pred_depth_ori).unsqueeze(0).to(self.DEVICE)
|
| 45 |
+
|
| 46 |
+
def save_raw_depth(self, depth, filepath):
|
| 47 |
+
depth_normalized = (depth / depth.max() * 60000).astype(np.uint16)
|
| 48 |
+
cv2.imwrite(filepath, depth_normalized)
|
| 49 |
+
|
| 50 |
+
def to(self, device):
|
| 51 |
+
self.DEVICE = device
|
| 52 |
+
self.depth_model = self.depth_model.to(device)
|
| 53 |
+
|
| 54 |
+
def delete(self):
|
| 55 |
+
del self.depth_model
|
scripts/deforum_helpers/depth_midas.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
from .general_utils import download_file_with_checksum
|
| 6 |
+
from midas.dpt_depth import DPTDepthModel
|
| 7 |
+
from midas.transforms import Resize, NormalizeImage, PrepareForNet
|
| 8 |
+
import torchvision.transforms as T
|
| 9 |
+
|
| 10 |
+
class MidasDepth:
|
| 11 |
+
def __init__(self, models_path, device, half_precision=True, midas_model_type='Midas-3-Hybrid'):
|
| 12 |
+
if midas_model_type.lower() == 'midas-3.1-beitlarge':
|
| 13 |
+
self.midas_model_filename = 'dpt_beit_large_512.pt'
|
| 14 |
+
self.midas_model_checksum='66cbb00ea7bccd6e43d3fd277bd21002d8d8c2c5c487e5fcd1e1d70c691688a19122418b3ddfa94e62ab9f086957aa67bbec39afe2b41c742aaaf0699ee50b33'
|
| 15 |
+
self.midas_model_url = 'https://github.com/isl-org/MiDaS/releases/download/v3_1/dpt_beit_large_512.pt'
|
| 16 |
+
self.resize_px = 512
|
| 17 |
+
self.backbone = 'beitl16_512'
|
| 18 |
+
else:
|
| 19 |
+
self.midas_model_filename = 'dpt_large-midas-2f21e586.pt'
|
| 20 |
+
self.midas_model_checksum = 'fcc4829e65d00eeed0a38e9001770676535d2e95c8a16965223aba094936e1316d569563552a852d471f310f83f597e8a238987a26a950d667815e08adaebc06'
|
| 21 |
+
self.midas_model_url = 'https://github.com/intel-isl/DPT/releases/download/1_0/dpt_large-midas-2f21e586.pt'
|
| 22 |
+
self.resize_px = 384
|
| 23 |
+
self.backbone = 'vitl16_384'
|
| 24 |
+
self.device = device
|
| 25 |
+
self.normalization = NormalizeImage(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
|
| 26 |
+
self.midas_transform = T.Compose([
|
| 27 |
+
Resize(self.resize_px, self.resize_px, resize_target=None, keep_aspect_ratio=True, ensure_multiple_of=32,
|
| 28 |
+
resize_method="minimal", image_interpolation_method=cv2.INTER_CUBIC),
|
| 29 |
+
self.normalization,
|
| 30 |
+
PrepareForNet()
|
| 31 |
+
])
|
| 32 |
+
|
| 33 |
+
download_file_with_checksum(url=self.midas_model_url, expected_checksum=self.midas_model_checksum, dest_folder=models_path, dest_filename=self.midas_model_filename)
|
| 34 |
+
|
| 35 |
+
self.load_midas_model(models_path, self.midas_model_filename)
|
| 36 |
+
if half_precision:
|
| 37 |
+
self.midas_model = self.midas_model.half()
|
| 38 |
+
|
| 39 |
+
def load_midas_model(self, models_path, midas_model_filename):
|
| 40 |
+
model_file = os.path.join(models_path, midas_model_filename)
|
| 41 |
+
print(f"Loading MiDaS model from {midas_model_filename}...")
|
| 42 |
+
self.midas_model = DPTDepthModel(
|
| 43 |
+
path=model_file,
|
| 44 |
+
backbone=self.backbone,
|
| 45 |
+
non_negative=True,
|
| 46 |
+
)
|
| 47 |
+
self.midas_model.eval().to(self.device, memory_format=torch.channels_last if self.device == torch.device("cuda") else None)
|
| 48 |
+
|
| 49 |
+
def predict(self, prev_img_cv2, half_precision):
|
| 50 |
+
img_midas = prev_img_cv2.astype(np.float32) / 255.0
|
| 51 |
+
img_midas_input = self.midas_transform({"image": img_midas})["image"]
|
| 52 |
+
sample = torch.from_numpy(img_midas_input).float().to(self.device).unsqueeze(0)
|
| 53 |
+
|
| 54 |
+
if self.device.type == "cuda" or self.device.type == "mps":
|
| 55 |
+
sample = sample.to(memory_format=torch.channels_last)
|
| 56 |
+
if half_precision:
|
| 57 |
+
sample = sample.half()
|
| 58 |
+
|
| 59 |
+
with torch.no_grad():
|
| 60 |
+
midas_depth = self.midas_model.forward(sample)
|
| 61 |
+
midas_depth = torch.nn.functional.interpolate(
|
| 62 |
+
midas_depth.unsqueeze(1),
|
| 63 |
+
size=img_midas.shape[:2],
|
| 64 |
+
mode="bicubic",
|
| 65 |
+
align_corners=False,
|
| 66 |
+
).squeeze().cpu().numpy()
|
| 67 |
+
|
| 68 |
+
torch.cuda.empty_cache()
|
| 69 |
+
depth_tensor = torch.from_numpy(np.expand_dims(midas_depth, axis=0)).squeeze().to(self.device)
|
| 70 |
+
|
| 71 |
+
return depth_tensor
|
| 72 |
+
|
| 73 |
+
def to(self, device):
|
| 74 |
+
self.device = device
|
| 75 |
+
self.midas_model = self.midas_model.to(device, memory_format=torch.channels_last if device == torch.device("cuda") else None)
|
scripts/deforum_helpers/depth_zoe.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from zoedepth.models.builder import build_model
|
| 3 |
+
from zoedepth.utils.config import get_config
|
| 4 |
+
|
| 5 |
+
class ZoeDepth:
|
| 6 |
+
def __init__(self, width=512, height=512):
|
| 7 |
+
conf = get_config("zoedepth_nk", "infer")
|
| 8 |
+
conf.img_size = [width, height]
|
| 9 |
+
self.model_zoe = build_model(conf)
|
| 10 |
+
self.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 11 |
+
self.zoe = self.model_zoe.to(self.DEVICE)
|
| 12 |
+
self.width = width
|
| 13 |
+
self.height = height
|
| 14 |
+
|
| 15 |
+
def predict(self, image):
|
| 16 |
+
self.zoe.core.prep.resizer._Resize__width = self.width
|
| 17 |
+
self.zoe.core.prep.resizer._Resize__height = self.height
|
| 18 |
+
depth_tensor = self.zoe.infer_pil(image, output_type="tensor")
|
| 19 |
+
return depth_tensor
|
| 20 |
+
|
| 21 |
+
def to(self, device):
|
| 22 |
+
self.DEVICE = device
|
| 23 |
+
self.zoe = self.model_zoe.to(device)
|
| 24 |
+
|
| 25 |
+
def save_raw_depth(self, depth, filepath):
|
| 26 |
+
depth.save(filepath, format='PNG', mode='I;16')
|
| 27 |
+
|
| 28 |
+
def delete(self):
|
| 29 |
+
del self.model_zoe
|
| 30 |
+
del self.zoe
|
scripts/deforum_helpers/frame_interpolation.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from rife.inference_video import run_rife_new_video_infer
|
| 4 |
+
from .video_audio_utilities import get_quick_vid_info, vid2frames, media_file_has_audio, extract_number, ffmpeg_stitch_video
|
| 5 |
+
from film_interpolation.film_inference import run_film_interp_infer
|
| 6 |
+
from .general_utils import duplicate_pngs_from_folder, checksum, convert_images_from_list
|
| 7 |
+
from modules.shared import opts
|
| 8 |
+
|
| 9 |
+
DEBUG_MODE = opts.data.get("deforum_debug_mode_enabled", False)
|
| 10 |
+
|
| 11 |
+
# gets 'RIFE v4.3', returns: 'RIFE43'
|
| 12 |
+
def extract_rife_name(string):
|
| 13 |
+
parts = string.split()
|
| 14 |
+
if len(parts) != 2 or parts[0] != "RIFE" or (parts[1][0] != "v" or not parts[1][1:].replace('.','').isdigit()):
|
| 15 |
+
raise ValueError("Input string should contain exactly 2 words, first word should be 'RIFE' and second word should start with 'v' followed by 2 numbers")
|
| 16 |
+
return "RIFE"+parts[1][1:].replace('.','')
|
| 17 |
+
|
| 18 |
+
# This function usually gets a filename, and converts it to a legal linux/windows *folder* name
|
| 19 |
+
def clean_folder_name(string):
|
| 20 |
+
illegal_chars = "/\\<>:\"|?*.,\" "
|
| 21 |
+
translation_table = str.maketrans(illegal_chars, "_"*len(illegal_chars))
|
| 22 |
+
return string.translate(translation_table)
|
| 23 |
+
|
| 24 |
+
def set_interp_out_fps(interp_x, slow_x_enabled, slom_x, in_vid_fps):
|
| 25 |
+
if interp_x == 'Disabled' or in_vid_fps in ('---', None, '', 'None'):
|
| 26 |
+
return '---'
|
| 27 |
+
|
| 28 |
+
fps = float(in_vid_fps) * int(interp_x)
|
| 29 |
+
# if slom_x != -1:
|
| 30 |
+
if slow_x_enabled:
|
| 31 |
+
fps /= int(slom_x)
|
| 32 |
+
return int(fps) if fps.is_integer() else fps
|
| 33 |
+
|
| 34 |
+
# get uploaded video frame count, fps, and return 3 valuees for the gradio UI: in fcount, in fps, out fps (using the set_interp_out_fps function above)
|
| 35 |
+
def gradio_f_interp_get_fps_and_fcount(vid_path, interp_x, slow_x_enabled, slom_x):
|
| 36 |
+
if vid_path is None:
|
| 37 |
+
return '---', '---', '---'
|
| 38 |
+
fps, fcount, resolution = get_quick_vid_info(vid_path.name)
|
| 39 |
+
expected_out_fps = set_interp_out_fps(interp_x, slow_x_enabled, slom_x, fps)
|
| 40 |
+
return (str(round(fps,2)) if fps is not None else '---', (round(fcount,2)) if fcount is not None else '---', round(expected_out_fps,2))
|
| 41 |
+
|
| 42 |
+
# handle call to interpolate an uploaded video from gradio button in args.py (the function that calls this func is named 'upload_vid_to_rife')
|
| 43 |
+
def process_interp_vid_upload_logic(file, engine, x_am, sl_enabled, sl_am, keep_imgs, f_location, f_crf, f_preset, in_vid_fps, f_models_path, vid_file_name):
|
| 44 |
+
|
| 45 |
+
print("got a request to *frame interpolate* an existing video.")
|
| 46 |
+
|
| 47 |
+
_, _, resolution = get_quick_vid_info(file.name)
|
| 48 |
+
folder_name = clean_folder_name(Path(vid_file_name).stem)
|
| 49 |
+
outdir = opts.outdir_samples or os.path.join(os.getcwd(), 'outputs')
|
| 50 |
+
outdir_no_tmp = outdir + f'/frame-interpolation/{folder_name}'
|
| 51 |
+
i = 1
|
| 52 |
+
while os.path.exists(outdir_no_tmp):
|
| 53 |
+
outdir_no_tmp = f"{outdir}/frame-interpolation/{folder_name}_{i}"
|
| 54 |
+
i += 1
|
| 55 |
+
|
| 56 |
+
outdir = os.path.join(outdir_no_tmp, 'tmp_input_frames')
|
| 57 |
+
os.makedirs(outdir, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
vid2frames(video_path=file.name, video_in_frame_path=outdir, overwrite=True, extract_from_frame=0, extract_to_frame=-1, numeric_files_output=True, out_img_format='png')
|
| 60 |
+
|
| 61 |
+
# check if the uploaded vid has an audio stream. If it doesn't, set audio param to None so that ffmpeg won't try to add non-existing audio to final video.
|
| 62 |
+
audio_file_to_pass = None
|
| 63 |
+
if media_file_has_audio(file.name, f_location):
|
| 64 |
+
audio_file_to_pass = file.name
|
| 65 |
+
|
| 66 |
+
process_video_interpolation(frame_interpolation_engine=engine, frame_interpolation_x_amount=x_am, frame_interpolation_slow_mo_enabled = sl_enabled,frame_interpolation_slow_mo_amount=sl_am, orig_vid_fps=in_vid_fps, deforum_models_path=f_models_path, real_audio_track=audio_file_to_pass, raw_output_imgs_path=outdir, img_batch_id=None, ffmpeg_location=f_location, ffmpeg_crf=f_crf, ffmpeg_preset=f_preset, keep_interp_imgs=keep_imgs, orig_vid_name=folder_name, resolution=resolution)
|
| 67 |
+
|
| 68 |
+
# handle params before talking with the actual interpolation module (rifee/film, more to be added)
|
| 69 |
+
def process_video_interpolation(frame_interpolation_engine, frame_interpolation_x_amount, frame_interpolation_slow_mo_enabled, frame_interpolation_slow_mo_amount, orig_vid_fps, deforum_models_path, real_audio_track, raw_output_imgs_path, img_batch_id, ffmpeg_location, ffmpeg_crf, ffmpeg_preset, keep_interp_imgs, orig_vid_name, resolution, dont_change_fps=False, srt_path=None):
|
| 70 |
+
|
| 71 |
+
is_random_pics_run = dont_change_fps
|
| 72 |
+
fps = float(orig_vid_fps) * (1 if is_random_pics_run else frame_interpolation_x_amount)
|
| 73 |
+
fps /= int(frame_interpolation_slow_mo_amount) if frame_interpolation_slow_mo_enabled and not is_random_pics_run else 1
|
| 74 |
+
|
| 75 |
+
# disable audio-adding by setting real_audio_track to None if slow-mo is enabled
|
| 76 |
+
if real_audio_track is not None and frame_interpolation_slow_mo_enabled:
|
| 77 |
+
real_audio_track = None
|
| 78 |
+
|
| 79 |
+
# disable subtitles by setting srt_path to None if slow-mo is enabled'
|
| 80 |
+
if srt_path is not None and frame_interpolation_slow_mo_enabled:
|
| 81 |
+
srt_path = None
|
| 82 |
+
|
| 83 |
+
if frame_interpolation_engine == 'None':
|
| 84 |
+
return
|
| 85 |
+
elif frame_interpolation_engine.startswith("RIFE"):
|
| 86 |
+
# make sure interp_x is valid and in range
|
| 87 |
+
if frame_interpolation_x_amount not in range(2, 11):
|
| 88 |
+
raise Error("frame_interpolation_x_amount must be between 2x and 10x")
|
| 89 |
+
|
| 90 |
+
# set UHD to True if res' is 2K or higher
|
| 91 |
+
if resolution:
|
| 92 |
+
UHD = resolution[0] >= 2048 and resolution[1] >= 2048
|
| 93 |
+
else:
|
| 94 |
+
UHD = False
|
| 95 |
+
# e.g from "RIFE v2.3 to RIFE23"
|
| 96 |
+
actual_model_folder_name = extract_rife_name(frame_interpolation_engine)
|
| 97 |
+
|
| 98 |
+
# run actual rife interpolation and video stitching etc - the whole suite
|
| 99 |
+
return run_rife_new_video_infer(interp_x_amount=frame_interpolation_x_amount, slow_mo_enabled = frame_interpolation_slow_mo_enabled, slow_mo_x_amount=frame_interpolation_slow_mo_amount, model=actual_model_folder_name, fps=fps, deforum_models_path=deforum_models_path, audio_track=real_audio_track, raw_output_imgs_path=raw_output_imgs_path, img_batch_id=img_batch_id, ffmpeg_location=ffmpeg_location, ffmpeg_crf=ffmpeg_crf, ffmpeg_preset=ffmpeg_preset, keep_imgs=keep_interp_imgs, orig_vid_name=orig_vid_name, UHD=UHD, srt_path=srt_path)
|
| 100 |
+
elif frame_interpolation_engine == 'FILM':
|
| 101 |
+
return prepare_film_inference(deforum_models_path=deforum_models_path, x_am=frame_interpolation_x_amount, sl_enabled=frame_interpolation_slow_mo_enabled, sl_am=frame_interpolation_slow_mo_amount, keep_imgs=keep_interp_imgs, raw_output_imgs_path=raw_output_imgs_path, img_batch_id=img_batch_id, f_location=ffmpeg_location, f_crf=ffmpeg_crf, f_preset=ffmpeg_preset, fps=fps, audio_track=real_audio_track, orig_vid_name=orig_vid_name, is_random_pics_run=is_random_pics_run, srt_path=srt_path)
|
| 102 |
+
else:
|
| 103 |
+
print("Unknown Frame Interpolation engine chosen. Doing nothing.")
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
def prepare_film_inference(deforum_models_path, x_am, sl_enabled, sl_am, keep_imgs, raw_output_imgs_path, img_batch_id, f_location, f_crf, f_preset, fps, audio_track, orig_vid_name, is_random_pics_run, srt_path=None):
|
| 107 |
+
import shutil
|
| 108 |
+
|
| 109 |
+
parent_folder = os.path.dirname(raw_output_imgs_path)
|
| 110 |
+
grandparent_folder = os.path.dirname(parent_folder)
|
| 111 |
+
if orig_vid_name is not None:
|
| 112 |
+
interp_vid_path = os.path.join(parent_folder, str(orig_vid_name) +'_FILM_x' + str(x_am))
|
| 113 |
+
else:
|
| 114 |
+
interp_vid_path = os.path.join(raw_output_imgs_path, str(img_batch_id) +'_FILM_x' + str(x_am))
|
| 115 |
+
|
| 116 |
+
film_model_name = 'film_net_fp16.pt'
|
| 117 |
+
film_model_folder = os.path.join(deforum_models_path,'film_interpolation')
|
| 118 |
+
film_model_path = os.path.join(film_model_folder, film_model_name) # actual full path to the film .pt model file
|
| 119 |
+
output_interp_imgs_folder = os.path.join(raw_output_imgs_path, 'interpolated_frames_film')
|
| 120 |
+
# set custom name depending on if we interpolate after a run, or interpolate a video (related/unrelated to deforum, we don't know) directly from within the interpolation tab
|
| 121 |
+
# interpolated_path = os.path.join(args.raw_output_imgs_path, 'interpolated_frames_rife')
|
| 122 |
+
if orig_vid_name is not None: # interpolating a video/ set of pictures (deforum or unrelated)
|
| 123 |
+
custom_interp_path = "{}_{}".format(output_interp_imgs_folder, orig_vid_name)
|
| 124 |
+
else: # interpolating after a deforum run:
|
| 125 |
+
custom_interp_path = "{}_{}".format(output_interp_imgs_folder, img_batch_id)
|
| 126 |
+
|
| 127 |
+
# interp_vid_path = os.path.join(raw_output_imgs_path, str(img_batch_id) + '_FILM_x' + str(x_am))
|
| 128 |
+
img_path_for_ffmpeg = os.path.join(custom_interp_path, "frame_%09d.png")
|
| 129 |
+
|
| 130 |
+
if sl_enabled:
|
| 131 |
+
interp_vid_path = interp_vid_path + '_slomo_x' + str(sl_am)
|
| 132 |
+
interp_vid_path = interp_vid_path + '.mp4'
|
| 133 |
+
|
| 134 |
+
# In this folder we temporarily keep the original frames (converted/ copy-pasted and img format depends on scenario)
|
| 135 |
+
temp_convert_raw_png_path = os.path.join(raw_output_imgs_path, "tmp_film_folder")
|
| 136 |
+
if is_random_pics_run: # pass dummy so it just copy-paste the imgs instead of re-writing them
|
| 137 |
+
total_frames = duplicate_pngs_from_folder(raw_output_imgs_path, temp_convert_raw_png_path, img_batch_id, 'DUMMY')
|
| 138 |
+
else: #re-write pics as png to avert a problem with 24 and 32 mixed outputs from the same animation run
|
| 139 |
+
total_frames = duplicate_pngs_from_folder(raw_output_imgs_path, temp_convert_raw_png_path, img_batch_id, None)
|
| 140 |
+
check_and_download_film_model('film_net_fp16.pt', film_model_folder) # TODO: split this part
|
| 141 |
+
|
| 142 |
+
# get number of in-between-frames to provide to FILM - mimics how RIFE works, we should get the same amount of total frames in the end
|
| 143 |
+
film_in_between_frames_count = calculate_frames_to_add(total_frames, x_am)
|
| 144 |
+
# Run actual FILM inference
|
| 145 |
+
run_film_interp_infer(
|
| 146 |
+
model_path = film_model_path,
|
| 147 |
+
input_folder = temp_convert_raw_png_path,
|
| 148 |
+
save_folder = custom_interp_path, # output folder is created in the infer part
|
| 149 |
+
inter_frames = film_in_between_frames_count)
|
| 150 |
+
|
| 151 |
+
add_soundtrack = 'None'
|
| 152 |
+
if not audio_track is None:
|
| 153 |
+
add_soundtrack = 'File'
|
| 154 |
+
|
| 155 |
+
print (f"*Passing interpolated frames to ffmpeg...*")
|
| 156 |
+
exception_raised = False
|
| 157 |
+
try:
|
| 158 |
+
ffmpeg_stitch_video(ffmpeg_location=f_location, fps=fps, outmp4_path=interp_vid_path, stitch_from_frame=0, stitch_to_frame=999999999, imgs_path=img_path_for_ffmpeg, add_soundtrack=add_soundtrack, audio_path=audio_track, crf=f_crf, preset=f_preset, srt_path=srt_path)
|
| 159 |
+
except Exception as e:
|
| 160 |
+
exception_raised = True
|
| 161 |
+
print(f"An error occurred while stitching the video: {e}")
|
| 162 |
+
|
| 163 |
+
if orig_vid_name and (keep_imgs or exception_raised):
|
| 164 |
+
shutil.move(custom_interp_path, parent_folder)
|
| 165 |
+
if not keep_imgs and not exception_raised:
|
| 166 |
+
if fps <= 450: # keep interp frames automatically if out_vid fps is above 450
|
| 167 |
+
shutil.rmtree(custom_interp_path, ignore_errors=True)
|
| 168 |
+
# delete duplicated raw non-interpolated frames
|
| 169 |
+
shutil.rmtree(temp_convert_raw_png_path, ignore_errors=True)
|
| 170 |
+
# remove folder with raw (non-interpolated) vid input frames in case of input VID and not PNGs
|
| 171 |
+
if orig_vid_name:
|
| 172 |
+
shutil.rmtree(raw_output_imgs_path, ignore_errors=True)
|
| 173 |
+
|
| 174 |
+
return interp_vid_path
|
| 175 |
+
|
| 176 |
+
def check_and_download_film_model(model_name, model_dest_folder):
|
| 177 |
+
from basicsr.utils.download_util import load_file_from_url
|
| 178 |
+
if model_name == 'film_net_fp16.pt':
|
| 179 |
+
model_dest_path = os.path.join(model_dest_folder, model_name)
|
| 180 |
+
download_url = 'https://github.com/hithereai/frame-interpolation-pytorch/releases/download/film_net_fp16.pt/film_net_fp16.pt'
|
| 181 |
+
film_model_hash = '0a823815b111488ac2b7dd7fe6acdd25d35a22b703e8253587764cf1ee3f8f93676d24154d9536d2ce5bc3b2f102fb36dfe0ca230dfbe289d5cd7bde5a34ec12'
|
| 182 |
+
else: # Unknown FILM model
|
| 183 |
+
raise Exception("Got a request to download an unknown FILM model. Can't proceed.")
|
| 184 |
+
if os.path.exists(model_dest_path):
|
| 185 |
+
return
|
| 186 |
+
try:
|
| 187 |
+
os.makedirs(model_dest_folder, exist_ok=True)
|
| 188 |
+
# download film model from url
|
| 189 |
+
load_file_from_url(download_url, model_dest_folder)
|
| 190 |
+
# verify checksum
|
| 191 |
+
if checksum(model_dest_path) != film_model_hash:
|
| 192 |
+
raise Exception(f"Error while downloading {model_name}. Please download from: {download_url}, and put in: {model_dest_folder}")
|
| 193 |
+
except Exception as e:
|
| 194 |
+
raise Exception(f"Error while downloading {model_name}. Please download from: {download_url}, and put in: {model_dest_folder}")
|
| 195 |
+
|
| 196 |
+
# get film no. of frames to add after each pic from tot frames in interp_x values
|
| 197 |
+
def calculate_frames_to_add(total_frames, interp_x):
|
| 198 |
+
frames_to_add = (total_frames * interp_x - total_frames) / (total_frames - 1)
|
| 199 |
+
return int(round(frames_to_add))
|
| 200 |
+
|
| 201 |
+
def process_interp_pics_upload_logic(pic_list, engine, x_am, sl_enabled, sl_am, keep_imgs, f_location, f_crf, f_preset, fps, f_models_path, resolution, add_soundtrack, audio_track):
|
| 202 |
+
pic_path_list = [pic.name for pic in pic_list]
|
| 203 |
+
print(f"got a request to *frame interpolate* a set of {len(pic_list)} images.")
|
| 204 |
+
folder_name = clean_folder_name(Path(pic_list[0].orig_name).stem)
|
| 205 |
+
outdir_no_tmp = os.path.join(os.getcwd(), 'outputs', 'frame-interpolation', folder_name)
|
| 206 |
+
i = 1
|
| 207 |
+
while os.path.exists(outdir_no_tmp):
|
| 208 |
+
outdir_no_tmp = os.path.join(os.getcwd(), 'outputs', 'frame-interpolation', folder_name + '_' + str(i))
|
| 209 |
+
i += 1
|
| 210 |
+
|
| 211 |
+
outdir = os.path.join(outdir_no_tmp, 'tmp_input_frames')
|
| 212 |
+
os.makedirs(outdir, exist_ok=True)
|
| 213 |
+
|
| 214 |
+
convert_images_from_list(paths=pic_path_list, output_dir=outdir,format='png')
|
| 215 |
+
|
| 216 |
+
audio_file_to_pass = None
|
| 217 |
+
# todo? add handling of vid input sound? if needed at all...
|
| 218 |
+
if add_soundtrack == 'File':
|
| 219 |
+
audio_file_to_pass = audio_track
|
| 220 |
+
# todo: upgrade function so it takes url and check if audio really exist before passing? not crucial as ffmpeg sofly fallbacks if needed
|
| 221 |
+
# if media_file_has_audio(audio_track, f_location):
|
| 222 |
+
|
| 223 |
+
# pass param so it won't duplicate the images at all as we already do it in here?!
|
| 224 |
+
process_video_interpolation(frame_interpolation_engine=engine, frame_interpolation_x_amount=x_am, frame_interpolation_slow_mo_enabled = sl_enabled,frame_interpolation_slow_mo_amount=sl_am, orig_vid_fps=fps, deforum_models_path=f_models_path, real_audio_track=audio_file_to_pass, raw_output_imgs_path=outdir, img_batch_id=None, ffmpeg_location=f_location, ffmpeg_crf=f_crf, ffmpeg_preset=f_preset, keep_interp_imgs=keep_imgs, orig_vid_name=folder_name, resolution=resolution, dont_change_fps=True)
|
scripts/deforum_helpers/general_utils.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import hashlib
|
| 4 |
+
from modules.shared import opts
|
| 5 |
+
from basicsr.utils.download_util import load_file_from_url
|
| 6 |
+
|
| 7 |
+
def debug_print(message):
|
| 8 |
+
DEBUG_MODE = opts.data.get("deforum_debug_mode_enabled", False)
|
| 9 |
+
if DEBUG_MODE:
|
| 10 |
+
print(message)
|
| 11 |
+
|
| 12 |
+
def checksum(filename, hash_factory=hashlib.blake2b, chunk_num_blocks=128):
|
| 13 |
+
h = hash_factory()
|
| 14 |
+
with open(filename,'rb') as f:
|
| 15 |
+
while chunk := f.read(chunk_num_blocks*h.block_size):
|
| 16 |
+
h.update(chunk)
|
| 17 |
+
return h.hexdigest()
|
| 18 |
+
|
| 19 |
+
def get_os():
|
| 20 |
+
import platform
|
| 21 |
+
return {"Windows": "Windows", "Linux": "Linux", "Darwin": "Mac"}.get(platform.system(), "Unknown")
|
| 22 |
+
|
| 23 |
+
# used in src/rife/inference_video.py and more, soon
|
| 24 |
+
def duplicate_pngs_from_folder(from_folder, to_folder, img_batch_id, orig_vid_name):
|
| 25 |
+
import cv2
|
| 26 |
+
#TODO: don't copy-paste at all if the input is a video (now it copy-pastes, and if input is deforum run is also converts to make sure no errors rise cuz of 24-32 bit depth differences)
|
| 27 |
+
temp_convert_raw_png_path = os.path.join(from_folder, to_folder)
|
| 28 |
+
os.makedirs(temp_convert_raw_png_path, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
frames_handled = 0
|
| 31 |
+
for f in os.listdir(from_folder):
|
| 32 |
+
if ('png' in f or 'jpg' in f) and '-' not in f and '_depth_' not in f and ((img_batch_id is not None and f.startswith(img_batch_id) or img_batch_id is None)):
|
| 33 |
+
frames_handled +=1
|
| 34 |
+
original_img_path = os.path.join(from_folder, f)
|
| 35 |
+
if orig_vid_name is not None:
|
| 36 |
+
shutil.copy(original_img_path, temp_convert_raw_png_path)
|
| 37 |
+
else:
|
| 38 |
+
image = cv2.imread(original_img_path)
|
| 39 |
+
new_path = os.path.join(temp_convert_raw_png_path, f)
|
| 40 |
+
cv2.imwrite(new_path, image, [cv2.IMWRITE_PNG_COMPRESSION, 0])
|
| 41 |
+
return frames_handled
|
| 42 |
+
|
| 43 |
+
def convert_images_from_list(paths, output_dir, format):
|
| 44 |
+
import os
|
| 45 |
+
from PIL import Image
|
| 46 |
+
# Ensure that the output directory exists
|
| 47 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
# Loop over all input images
|
| 50 |
+
for i, path in enumerate(paths):
|
| 51 |
+
# Open the image
|
| 52 |
+
with Image.open(path) as img:
|
| 53 |
+
# Generate the output filename
|
| 54 |
+
filename = f"{i+1:09d}.{format}"
|
| 55 |
+
# Save the image to the output directory
|
| 56 |
+
img.save(os.path.join(output_dir, filename))
|
| 57 |
+
|
| 58 |
+
def get_deforum_version():
|
| 59 |
+
from modules import extensions as mext
|
| 60 |
+
try:
|
| 61 |
+
for ext in mext.extensions:
|
| 62 |
+
if ext.name in ["deforum", "deforum-for-automatic1111-webui", "sd-webui-deforum"] and ext.enabled:
|
| 63 |
+
ext.read_info_from_repo() # need this call to get exten info on ui-launch, not to be removed
|
| 64 |
+
return ext.version
|
| 65 |
+
return "Unknown"
|
| 66 |
+
except:
|
| 67 |
+
return "Unknown"
|
| 68 |
+
|
| 69 |
+
def custom_placeholder_format(value_dict, placeholder_match):
|
| 70 |
+
key = placeholder_match.group(1).lower()
|
| 71 |
+
value = value_dict.get(key, key) or "_"
|
| 72 |
+
if isinstance(value, dict) and value:
|
| 73 |
+
first_key = list(value.keys())[0]
|
| 74 |
+
value = str(value[first_key][0]) if isinstance(value[first_key], list) and value[first_key] else str(value[first_key])
|
| 75 |
+
return str(value)[:50]
|
| 76 |
+
|
| 77 |
+
def test_long_path_support(base_folder_path):
|
| 78 |
+
long_folder_name = 'A' * 300
|
| 79 |
+
long_path = os.path.join(base_folder_path, long_folder_name)
|
| 80 |
+
try:
|
| 81 |
+
os.makedirs(long_path)
|
| 82 |
+
shutil.rmtree(long_path)
|
| 83 |
+
return True
|
| 84 |
+
except OSError:
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
+
def get_max_path_length(base_folder_path):
|
| 88 |
+
if get_os() == 'Windows':
|
| 89 |
+
return (32767 if test_long_path_support(base_folder_path) else 260) - len(base_folder_path) - 1
|
| 90 |
+
return 4096 - len(base_folder_path) - 1
|
| 91 |
+
|
| 92 |
+
def substitute_placeholders(template, arg_list, base_folder_path):
|
| 93 |
+
import re
|
| 94 |
+
# Find and update timestring values if resume_from_timestring is True
|
| 95 |
+
resume_from_timestring = next((arg_obj.resume_from_timestring for arg_obj in arg_list if hasattr(arg_obj, 'resume_from_timestring')), False)
|
| 96 |
+
resume_timestring = next((arg_obj.resume_timestring for arg_obj in arg_list if hasattr(arg_obj, 'resume_timestring')), None)
|
| 97 |
+
|
| 98 |
+
if resume_from_timestring and resume_timestring:
|
| 99 |
+
for arg_obj in arg_list:
|
| 100 |
+
if hasattr(arg_obj, 'timestring'):
|
| 101 |
+
arg_obj.timestring = resume_timestring
|
| 102 |
+
|
| 103 |
+
max_length = get_max_path_length(base_folder_path)
|
| 104 |
+
values = {attr.lower(): getattr(arg_obj, attr)
|
| 105 |
+
for arg_obj in arg_list
|
| 106 |
+
for attr in dir(arg_obj) if not callable(getattr(arg_obj, attr)) and not attr.startswith('__')}
|
| 107 |
+
formatted_string = re.sub(r"{(\w+)}", lambda m: custom_placeholder_format(values, m), template)
|
| 108 |
+
formatted_string = re.sub(r'[<>:"/\\|?*\s,]', '_', formatted_string)
|
| 109 |
+
return formatted_string[:max_length]
|
| 110 |
+
|
| 111 |
+
def count_files_in_folder(folder_path):
|
| 112 |
+
import glob
|
| 113 |
+
file_pattern = folder_path + "/*"
|
| 114 |
+
file_count = len(glob.glob(file_pattern))
|
| 115 |
+
return file_count
|
| 116 |
+
|
| 117 |
+
def clean_gradio_path_strings(input_str):
|
| 118 |
+
if isinstance(input_str, str) and input_str.startswith('"') and input_str.endswith('"'):
|
| 119 |
+
return input_str[1:-1]
|
| 120 |
+
else:
|
| 121 |
+
return input_str
|
| 122 |
+
|
| 123 |
+
def download_file_with_checksum(url, expected_checksum, dest_folder, dest_filename):
|
| 124 |
+
expected_full_path = os.path.join(dest_folder, dest_filename)
|
| 125 |
+
if not os.path.exists(expected_full_path) and not os.path.isdir(expected_full_path):
|
| 126 |
+
load_file_from_url(url=url, model_dir=dest_folder, file_name=dest_filename, progress=True)
|
| 127 |
+
if checksum(expected_full_path) != expected_checksum:
|
| 128 |
+
raise Exception(f"Error while downloading {dest_filename}.]nPlease manually download from: {url}\nAnd place it in: {dest_folder}")
|
scripts/deforum_helpers/generate.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image
|
| 2 |
+
import math
|
| 3 |
+
import json
|
| 4 |
+
import itertools
|
| 5 |
+
import requests
|
| 6 |
+
import numexpr
|
| 7 |
+
from modules import processing, sd_models
|
| 8 |
+
from modules.shared import sd_model, state, cmd_opts
|
| 9 |
+
from .deforum_controlnet import is_controlnet_enabled, process_with_controlnet
|
| 10 |
+
from .prompt import split_weighted_subprompts
|
| 11 |
+
from .load_images import load_img, prepare_mask, check_mask_for_errors
|
| 12 |
+
from .webui_sd_pipeline import get_webui_sd_pipeline
|
| 13 |
+
from .rich import console
|
| 14 |
+
from .defaults import get_samplers_list
|
| 15 |
+
from .prompt import check_is_number
|
| 16 |
+
from .general_utils import debug_print
|
| 17 |
+
|
| 18 |
+
def load_mask_latent(mask_input, shape):
|
| 19 |
+
# mask_input (str or PIL Image.Image): Path to the mask image or a PIL Image object
|
| 20 |
+
# shape (list-like len(4)): shape of the image to match, usually latent_image.shape
|
| 21 |
+
|
| 22 |
+
if isinstance(mask_input, str): # mask input is probably a file name
|
| 23 |
+
if mask_input.startswith('http://') or mask_input.startswith('https://'):
|
| 24 |
+
mask_image = Image.open(requests.get(mask_input, stream=True).raw).convert('RGBA')
|
| 25 |
+
else:
|
| 26 |
+
mask_image = Image.open(mask_input).convert('RGBA')
|
| 27 |
+
elif isinstance(mask_input, Image.Image):
|
| 28 |
+
mask_image = mask_input
|
| 29 |
+
else:
|
| 30 |
+
raise Exception("mask_input must be a PIL image or a file name")
|
| 31 |
+
|
| 32 |
+
mask_w_h = (shape[-1], shape[-2])
|
| 33 |
+
mask = mask_image.resize(mask_w_h, resample=Image.LANCZOS)
|
| 34 |
+
mask = mask.convert("L")
|
| 35 |
+
return mask
|
| 36 |
+
|
| 37 |
+
def isJson(myjson):
|
| 38 |
+
try:
|
| 39 |
+
json.loads(myjson)
|
| 40 |
+
except ValueError as e:
|
| 41 |
+
return False
|
| 42 |
+
return True
|
| 43 |
+
|
| 44 |
+
# Add pairwise implementation here not to upgrade
|
| 45 |
+
# the whole python to 3.10 just for one function
|
| 46 |
+
def pairwise_repl(iterable):
|
| 47 |
+
a, b = itertools.tee(iterable)
|
| 48 |
+
next(b, None)
|
| 49 |
+
return zip(a, b)
|
| 50 |
+
|
| 51 |
+
def generate(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame=0, sampler_name=None):
|
| 52 |
+
if state.interrupted:
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
if args.reroll_blank_frames == 'ignore':
|
| 56 |
+
return generate_inner(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame, sampler_name)
|
| 57 |
+
|
| 58 |
+
image, caught_vae_exception = generate_with_nans_check(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame, sampler_name)
|
| 59 |
+
|
| 60 |
+
if caught_vae_exception or not image.getbbox():
|
| 61 |
+
patience = args.reroll_patience
|
| 62 |
+
print("Blank frame detected! If you don't have the NSFW filter enabled, this may be due to a glitch!")
|
| 63 |
+
if args.reroll_blank_frames == 'reroll':
|
| 64 |
+
while caught_vae_exception or not image.getbbox():
|
| 65 |
+
print("Rerolling with +1 seed...")
|
| 66 |
+
args.seed += 1
|
| 67 |
+
image, caught_vae_exception = generate_with_nans_check(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame, sampler_name)
|
| 68 |
+
patience -= 1
|
| 69 |
+
if patience == 0:
|
| 70 |
+
print("Rerolling with +1 seed failed for 10 iterations! Try setting webui's precision to 'full' and if it fails, please report this to the devs! Interrupting...")
|
| 71 |
+
state.interrupted = True
|
| 72 |
+
state.current_image = image
|
| 73 |
+
return None
|
| 74 |
+
elif args.reroll_blank_frames == 'interrupt':
|
| 75 |
+
print("Interrupting to save your eyes...")
|
| 76 |
+
state.interrupted = True
|
| 77 |
+
state.current_image = image
|
| 78 |
+
return None
|
| 79 |
+
return image
|
| 80 |
+
|
| 81 |
+
def generate_with_nans_check(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame=0, sampler_name=None):
|
| 82 |
+
if cmd_opts.disable_nan_check:
|
| 83 |
+
image = generate_inner(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame, sampler_name)
|
| 84 |
+
else:
|
| 85 |
+
try:
|
| 86 |
+
image = generate_inner(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame, sampler_name)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
if "A tensor with all NaNs was produced in VAE." in repr(e):
|
| 89 |
+
print(e)
|
| 90 |
+
return None, True
|
| 91 |
+
else:
|
| 92 |
+
raise e
|
| 93 |
+
return image, False
|
| 94 |
+
|
| 95 |
+
def generate_inner(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame=0, sampler_name=None):
|
| 96 |
+
# Setup the pipeline
|
| 97 |
+
p = get_webui_sd_pipeline(args, root)
|
| 98 |
+
p.prompt, p.negative_prompt = split_weighted_subprompts(args.prompt, frame, anim_args.max_frames)
|
| 99 |
+
|
| 100 |
+
if not args.use_init and args.strength > 0 and args.strength_0_no_init:
|
| 101 |
+
args.strength = 0
|
| 102 |
+
processed = None
|
| 103 |
+
mask_image = None
|
| 104 |
+
init_image = None
|
| 105 |
+
image_init0 = None
|
| 106 |
+
|
| 107 |
+
if loop_args.use_looper and anim_args.animation_mode in ['2D', '3D']:
|
| 108 |
+
|
| 109 |
+
debug_print(f"Looper: use_looper={loop_args.use_looper}, imageStrength={loop_args.imageStrength}, blendFactorMax={loop_args.blendFactorMax}, blendFactorSlope={loop_args.blendFactorSlope}, tweeningFrames={loop_args.tweeningFrameSchedule}, colorCorrectionFactor={loop_args.colorCorrectionFactor}")
|
| 110 |
+
args.strength = loop_args.imageStrength
|
| 111 |
+
tweeningFrames = loop_args.tweeningFrameSchedule
|
| 112 |
+
blendFactor = .07
|
| 113 |
+
colorCorrectionFactor = loop_args.colorCorrectionFactor
|
| 114 |
+
jsonImages = json.loads(loop_args.imagesToKeyframe)
|
| 115 |
+
# find which image to show
|
| 116 |
+
parsedImages = {}
|
| 117 |
+
frameToChoose = 0
|
| 118 |
+
max_f = anim_args.max_frames - 1
|
| 119 |
+
|
| 120 |
+
for key, value in jsonImages.items():
|
| 121 |
+
if check_is_number(key): # default case 0:(1 + t %5), 30:(5-t%2)
|
| 122 |
+
parsedImages[key] = value
|
| 123 |
+
else: # math on the left hand side case 0:(1 + t %5), maxKeyframes/2:(5-t%2)
|
| 124 |
+
parsedImages[int(numexpr.evaluate(key))] = value
|
| 125 |
+
|
| 126 |
+
framesToImageSwapOn = list(map(int, list(parsedImages.keys())))
|
| 127 |
+
|
| 128 |
+
for swappingFrame in framesToImageSwapOn[1:]:
|
| 129 |
+
frameToChoose += (frame >= int(swappingFrame))
|
| 130 |
+
|
| 131 |
+
# find which frame to do our swapping on for tweening
|
| 132 |
+
skipFrame = 25
|
| 133 |
+
for fs, fe in pairwise_repl(framesToImageSwapOn):
|
| 134 |
+
if fs <= frame <= fe:
|
| 135 |
+
skipFrame = fe - fs
|
| 136 |
+
|
| 137 |
+
if frame % skipFrame <= tweeningFrames: # number of tweening frames
|
| 138 |
+
blendFactor = loop_args.blendFactorMax - loop_args.blendFactorSlope * math.cos((frame % tweeningFrames) / (tweeningFrames / 2))
|
| 139 |
+
init_image2, _ = load_img(list(jsonImages.values())[frameToChoose],
|
| 140 |
+
shape=(args.W, args.H),
|
| 141 |
+
use_alpha_as_mask=args.use_alpha_as_mask)
|
| 142 |
+
image_init0 = list(jsonImages.values())[0]
|
| 143 |
+
|
| 144 |
+
else: # they passed in a single init image
|
| 145 |
+
image_init0 = args.init_image
|
| 146 |
+
|
| 147 |
+
available_samplers = get_samplers_list()
|
| 148 |
+
if sampler_name is not None:
|
| 149 |
+
if sampler_name in available_samplers.keys():
|
| 150 |
+
p.sampler_name = available_samplers[sampler_name]
|
| 151 |
+
else:
|
| 152 |
+
raise RuntimeError(f"Sampler name '{sampler_name}' is invalid. Please check the available sampler list in the 'Run' tab")
|
| 153 |
+
|
| 154 |
+
if args.checkpoint is not None:
|
| 155 |
+
info = sd_models.get_closet_checkpoint_match(args.checkpoint)
|
| 156 |
+
if info is None:
|
| 157 |
+
raise RuntimeError(f"Unknown checkpoint: {args.checkpoint}")
|
| 158 |
+
sd_models.reload_model_weights(info=info)
|
| 159 |
+
|
| 160 |
+
if root.init_sample is not None:
|
| 161 |
+
# TODO: cleanup init_sample remains later
|
| 162 |
+
img = root.init_sample
|
| 163 |
+
init_image = img
|
| 164 |
+
image_init0 = img
|
| 165 |
+
if loop_args.use_looper and isJson(loop_args.imagesToKeyframe) and anim_args.animation_mode in ['2D', '3D']:
|
| 166 |
+
init_image = Image.blend(init_image, init_image2, blendFactor)
|
| 167 |
+
correction_colors = Image.blend(init_image, init_image2, colorCorrectionFactor)
|
| 168 |
+
p.color_corrections = [processing.setup_color_correction(correction_colors)]
|
| 169 |
+
|
| 170 |
+
# this is the first pass
|
| 171 |
+
elif (loop_args.use_looper and anim_args.animation_mode in ['2D', '3D']) or (args.use_init and ((args.init_image != None and args.init_image != ''))):
|
| 172 |
+
init_image, mask_image = load_img(image_init0, # initial init image
|
| 173 |
+
shape=(args.W, args.H),
|
| 174 |
+
use_alpha_as_mask=args.use_alpha_as_mask)
|
| 175 |
+
|
| 176 |
+
else:
|
| 177 |
+
|
| 178 |
+
if anim_args.animation_mode != 'Interpolation':
|
| 179 |
+
print(f"Not using an init image (doing pure txt2img)")
|
| 180 |
+
p_txt = processing.StableDiffusionProcessingTxt2Img(
|
| 181 |
+
sd_model=sd_model,
|
| 182 |
+
outpath_samples=root.tmp_deforum_run_duplicated_folder,
|
| 183 |
+
outpath_grids=root.tmp_deforum_run_duplicated_folder,
|
| 184 |
+
prompt=p.prompt,
|
| 185 |
+
styles=p.styles,
|
| 186 |
+
negative_prompt=p.negative_prompt,
|
| 187 |
+
seed=p.seed,
|
| 188 |
+
subseed=p.subseed,
|
| 189 |
+
subseed_strength=p.subseed_strength,
|
| 190 |
+
seed_resize_from_h=p.seed_resize_from_h,
|
| 191 |
+
seed_resize_from_w=p.seed_resize_from_w,
|
| 192 |
+
sampler_name=p.sampler_name,
|
| 193 |
+
batch_size=p.batch_size,
|
| 194 |
+
n_iter=p.n_iter,
|
| 195 |
+
steps=p.steps,
|
| 196 |
+
cfg_scale=p.cfg_scale,
|
| 197 |
+
width=p.width,
|
| 198 |
+
height=p.height,
|
| 199 |
+
restore_faces=p.restore_faces,
|
| 200 |
+
tiling=p.tiling,
|
| 201 |
+
enable_hr=False,
|
| 202 |
+
denoising_strength=0,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
print_combined_table(args, anim_args, p_txt, keys, frame) # print dynamic table to cli
|
| 206 |
+
|
| 207 |
+
if is_controlnet_enabled(controlnet_args):
|
| 208 |
+
process_with_controlnet(p_txt, args, anim_args, controlnet_args, root, parseq_adapter, is_img2img=False, frame_idx=frame)
|
| 209 |
+
|
| 210 |
+
processed = processing.process_images(p_txt)
|
| 211 |
+
|
| 212 |
+
if processed is None:
|
| 213 |
+
# Mask functions
|
| 214 |
+
if args.use_mask:
|
| 215 |
+
mask_image = args.mask_image
|
| 216 |
+
mask = prepare_mask(args.mask_file if mask_image is None else mask_image,
|
| 217 |
+
(args.W, args.H),
|
| 218 |
+
args.mask_contrast_adjust,
|
| 219 |
+
args.mask_brightness_adjust)
|
| 220 |
+
p.inpainting_mask_invert = args.invert_mask
|
| 221 |
+
p.inpainting_fill = args.fill
|
| 222 |
+
p.inpaint_full_res = args.full_res_mask
|
| 223 |
+
p.inpaint_full_res_padding = args.full_res_mask_padding
|
| 224 |
+
# prevent loaded mask from throwing errors in Image operations if completely black and crop and resize in webui pipeline
|
| 225 |
+
# doing this after contrast and brightness adjustments to ensure that mask is not passed as black or blank
|
| 226 |
+
mask = check_mask_for_errors(mask, args.invert_mask)
|
| 227 |
+
root.noise_mask = mask
|
| 228 |
+
else:
|
| 229 |
+
mask = None
|
| 230 |
+
|
| 231 |
+
assert not ((mask is not None and args.use_mask and args.overlay_mask) and (
|
| 232 |
+
root.init_sample is None and init_image is None)), "Need an init image when use_mask == True and overlay_mask == True"
|
| 233 |
+
|
| 234 |
+
p.init_images = [init_image]
|
| 235 |
+
p.image_mask = mask
|
| 236 |
+
p.image_cfg_scale = args.pix2pix_img_cfg_scale
|
| 237 |
+
|
| 238 |
+
print_combined_table(args, anim_args, p, keys, frame) # print dynamic table to cli
|
| 239 |
+
|
| 240 |
+
if is_controlnet_enabled(controlnet_args):
|
| 241 |
+
process_with_controlnet(p, args, anim_args, controlnet_args, root, parseq_adapter, is_img2img=True, frame_idx=frame)
|
| 242 |
+
|
| 243 |
+
processed = processing.process_images(p)
|
| 244 |
+
|
| 245 |
+
if root.initial_info is None:
|
| 246 |
+
root.initial_info = processed.info
|
| 247 |
+
|
| 248 |
+
if root.first_frame is None:
|
| 249 |
+
root.first_frame = processed.images[0]
|
| 250 |
+
|
| 251 |
+
results = processed.images[0]
|
| 252 |
+
|
| 253 |
+
return results
|
| 254 |
+
|
| 255 |
+
def print_combined_table(args, anim_args, p, keys, frame_idx):
|
| 256 |
+
from rich.table import Table
|
| 257 |
+
from rich import box
|
| 258 |
+
|
| 259 |
+
table = Table(padding=0, box=box.ROUNDED)
|
| 260 |
+
|
| 261 |
+
field_names1 = ["Steps", "CFG"]
|
| 262 |
+
if anim_args.animation_mode != 'Interpolation':
|
| 263 |
+
field_names1.append("Denoise")
|
| 264 |
+
field_names1 += ["Subseed", "Subs. str"] * (anim_args.enable_subseed_scheduling)
|
| 265 |
+
field_names1 += ["Sampler"] * anim_args.enable_sampler_scheduling
|
| 266 |
+
field_names1 += ["Checkpoint"] * anim_args.enable_checkpoint_scheduling
|
| 267 |
+
|
| 268 |
+
for field_name in field_names1:
|
| 269 |
+
table.add_column(field_name, justify="center")
|
| 270 |
+
|
| 271 |
+
rows1 = [str(p.steps), str(p.cfg_scale)]
|
| 272 |
+
if anim_args.animation_mode != 'Interpolation':
|
| 273 |
+
rows1.append(f"{p.denoising_strength:.5g}" if p.denoising_strength is not None else "None")
|
| 274 |
+
|
| 275 |
+
rows1 += [str(p.subseed), f"{p.subseed_strength:.5g}"] * anim_args.enable_subseed_scheduling
|
| 276 |
+
rows1 += [p.sampler_name] * anim_args.enable_sampler_scheduling
|
| 277 |
+
rows1 += [str(args.checkpoint)] * anim_args.enable_checkpoint_scheduling
|
| 278 |
+
|
| 279 |
+
rows2 = []
|
| 280 |
+
if anim_args.animation_mode not in ['Video Input', 'Interpolation']:
|
| 281 |
+
if anim_args.animation_mode == '2D':
|
| 282 |
+
field_names2 = ["Angle", "Zoom"]
|
| 283 |
+
else:
|
| 284 |
+
field_names2 = []
|
| 285 |
+
field_names2 += ["Tr X", "Tr Y"]
|
| 286 |
+
if anim_args.animation_mode == '3D':
|
| 287 |
+
field_names2 += ["Tr Z", "Ro X", "Ro Y", "Ro Z"]
|
| 288 |
+
if anim_args.aspect_ratio_schedule.replace(" ", "") != '0:(1)':
|
| 289 |
+
field_names2 += ["Asp. Ratio"]
|
| 290 |
+
if anim_args.enable_perspective_flip:
|
| 291 |
+
field_names2 += ["Pf T", "Pf P", "Pf G", "Pf F"]
|
| 292 |
+
|
| 293 |
+
for field_name in field_names2:
|
| 294 |
+
table.add_column(field_name, justify="center")
|
| 295 |
+
|
| 296 |
+
if anim_args.animation_mode == '2D':
|
| 297 |
+
rows2 += [f"{keys.angle_series[frame_idx]:.5g}", f"{keys.zoom_series[frame_idx]:.5g}"]
|
| 298 |
+
rows2 += [f"{keys.translation_x_series[frame_idx]:.5g}", f"{keys.translation_y_series[frame_idx]:.5g}"]
|
| 299 |
+
|
| 300 |
+
if anim_args.animation_mode == '3D':
|
| 301 |
+
rows2 += [f"{keys.translation_z_series[frame_idx]:.5g}", f"{keys.rotation_3d_x_series[frame_idx]:.5g}",
|
| 302 |
+
f"{keys.rotation_3d_y_series[frame_idx]:.5g}", f"{keys.rotation_3d_z_series[frame_idx]:.5g}"]
|
| 303 |
+
if anim_args.aspect_ratio_schedule.replace(" ", "") != '0:(1)':
|
| 304 |
+
rows2 += [f"{keys.aspect_ratio_series[frame_idx]:.5g}"]
|
| 305 |
+
if anim_args.enable_perspective_flip:
|
| 306 |
+
rows2 += [f"{keys.perspective_flip_theta_series[frame_idx]:.5g}", f"{keys.perspective_flip_phi_series[frame_idx]:.5g}",
|
| 307 |
+
f"{keys.perspective_flip_gamma_series[frame_idx]:.5g}", f"{keys.perspective_flip_fv_series[frame_idx]:.5g}"]
|
| 308 |
+
|
| 309 |
+
table.add_row(*rows1, *rows2)
|
| 310 |
+
console.print(table)
|
scripts/deforum_helpers/gradio_funcs.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import modules.paths as ph
|
| 3 |
+
from .general_utils import get_os
|
| 4 |
+
from .upscaling import process_ncnn_upscale_vid_upload_logic
|
| 5 |
+
from .video_audio_utilities import extract_number, get_quick_vid_info, get_ffmpeg_params
|
| 6 |
+
from .frame_interpolation import process_interp_vid_upload_logic, process_interp_pics_upload_logic, gradio_f_interp_get_fps_and_fcount
|
| 7 |
+
from .vid2depth import process_depth_vid_upload_logic
|
| 8 |
+
|
| 9 |
+
f_models_path = ph.models_path + '/Deforum'
|
| 10 |
+
|
| 11 |
+
def handle_change_functions(l_vars):
|
| 12 |
+
l_vars['override_settings_with_file'].change(fn=hide_if_false, inputs=l_vars['override_settings_with_file'], outputs=l_vars['custom_settings_file'])
|
| 13 |
+
l_vars['sampler'].change(fn=show_when_ddim, inputs=l_vars['sampler'], outputs=l_vars['enable_ddim_eta_scheduling'])
|
| 14 |
+
l_vars['sampler'].change(fn=show_when_ancestral_samplers, inputs=l_vars['sampler'], outputs=l_vars['enable_ancestral_eta_scheduling'])
|
| 15 |
+
l_vars['enable_ancestral_eta_scheduling'].change(fn=hide_if_false, inputs=l_vars['enable_ancestral_eta_scheduling'], outputs=l_vars['ancestral_eta_schedule'])
|
| 16 |
+
l_vars['enable_ddim_eta_scheduling'].change(fn=hide_if_false, inputs=l_vars['enable_ddim_eta_scheduling'], outputs=l_vars['ddim_eta_schedule'])
|
| 17 |
+
l_vars['animation_mode'].change(fn=change_max_frames_visibility, inputs=l_vars['animation_mode'], outputs=l_vars['max_frames'])
|
| 18 |
+
diffusion_cadence_outputs = [l_vars['diffusion_cadence'], l_vars['guided_images_accord'], l_vars['optical_flow_cadence_row'], l_vars['cadence_flow_factor_schedule'],
|
| 19 |
+
l_vars['optical_flow_redo_generation'], l_vars['redo_flow_factor_schedule'], l_vars['diffusion_redo']]
|
| 20 |
+
for output in diffusion_cadence_outputs:
|
| 21 |
+
l_vars['animation_mode'].change(fn=change_diffusion_cadence_visibility, inputs=l_vars['animation_mode'], outputs=output)
|
| 22 |
+
three_d_related_outputs = [l_vars['only_3d_motion_column'], l_vars['depth_warp_row_1'], l_vars['depth_warp_row_2'], l_vars['depth_warp_row_3'], l_vars['depth_warp_row_4'],
|
| 23 |
+
l_vars['depth_warp_row_5'], l_vars['depth_warp_row_6'], l_vars['depth_warp_row_7']]
|
| 24 |
+
for output in three_d_related_outputs:
|
| 25 |
+
l_vars['animation_mode'].change(fn=disble_3d_related_stuff, inputs=l_vars['animation_mode'], outputs=output)
|
| 26 |
+
pers_flip_outputs = [l_vars['per_f_th_row'], l_vars['per_f_ph_row'], l_vars['per_f_ga_row'], l_vars['per_f_f_row']]
|
| 27 |
+
for output in pers_flip_outputs:
|
| 28 |
+
l_vars['enable_perspective_flip'].change(fn=hide_if_false, inputs=l_vars['enable_perspective_flip'], outputs=output)
|
| 29 |
+
l_vars['animation_mode'].change(fn=per_flip_handle, inputs=[l_vars['animation_mode'], l_vars['enable_perspective_flip']], outputs=output)
|
| 30 |
+
l_vars['animation_mode'].change(fn=only_show_in_non_3d_mode, inputs=l_vars['animation_mode'], outputs=l_vars['depth_warp_msg_html'])
|
| 31 |
+
l_vars['animation_mode'].change(fn=enable_2d_related_stuff, inputs=l_vars['animation_mode'], outputs=l_vars['only_2d_motion_column'])
|
| 32 |
+
l_vars['animation_mode'].change(fn=disable_by_interpolation, inputs=l_vars['animation_mode'], outputs=l_vars['color_force_grayscale'])
|
| 33 |
+
l_vars['animation_mode'].change(fn=disable_by_interpolation, inputs=l_vars['animation_mode'], outputs=l_vars['noise_tab_column'])
|
| 34 |
+
l_vars['animation_mode'].change(fn=disable_pers_flip_accord, inputs=l_vars['animation_mode'], outputs=l_vars['enable_per_f_row'])
|
| 35 |
+
l_vars['animation_mode'].change(fn=disable_pers_flip_accord, inputs=l_vars['animation_mode'], outputs=l_vars['both_anim_mode_motion_params_column'])
|
| 36 |
+
l_vars['aspect_ratio_use_old_formula'].change(fn=hide_if_true, inputs=l_vars['aspect_ratio_use_old_formula'], outputs=l_vars['aspect_ratio_schedule'])
|
| 37 |
+
l_vars['animation_mode'].change(fn=show_hybrid_html_msg, inputs=l_vars['animation_mode'], outputs=l_vars['hybrid_msg_html'])
|
| 38 |
+
l_vars['animation_mode'].change(fn=change_hybrid_tab_status, inputs=l_vars['animation_mode'], outputs=l_vars['hybrid_sch_accord'])
|
| 39 |
+
l_vars['animation_mode'].change(fn=change_hybrid_tab_status, inputs=l_vars['animation_mode'], outputs=l_vars['hybrid_settings_accord'])
|
| 40 |
+
l_vars['animation_mode'].change(fn=change_hybrid_tab_status, inputs=l_vars['animation_mode'], outputs=l_vars['humans_masking_accord'])
|
| 41 |
+
l_vars['optical_flow_redo_generation'].change(fn=hide_if_none, inputs=l_vars['optical_flow_redo_generation'], outputs=l_vars['redo_flow_factor_schedule_column'])
|
| 42 |
+
l_vars['optical_flow_cadence'].change(fn=hide_if_none, inputs=l_vars['optical_flow_cadence'], outputs=l_vars['cadence_flow_factor_schedule_column'])
|
| 43 |
+
l_vars['seed_behavior'].change(fn=change_seed_iter_visibility, inputs=l_vars['seed_behavior'], outputs=l_vars['seed_iter_N_row'])
|
| 44 |
+
l_vars['seed_behavior'].change(fn=change_seed_schedule_visibility, inputs=l_vars['seed_behavior'], outputs=l_vars['seed_schedule_row'])
|
| 45 |
+
l_vars['color_coherence'].change(fn=change_color_coherence_video_every_N_frames_visibility, inputs=l_vars['color_coherence'], outputs=l_vars['color_coherence_video_every_N_frames_row'])
|
| 46 |
+
l_vars['color_coherence'].change(fn=change_color_coherence_image_path_visibility, inputs=l_vars['color_coherence'], outputs=l_vars['color_coherence_image_path_row'])
|
| 47 |
+
l_vars['noise_type'].change(fn=change_perlin_visibility, inputs=l_vars['noise_type'], outputs=l_vars['perlin_row'])
|
| 48 |
+
l_vars['diffusion_cadence'].change(fn=hide_optical_flow_cadence, inputs=l_vars['diffusion_cadence'], outputs=l_vars['optical_flow_cadence_row'])
|
| 49 |
+
l_vars['depth_algorithm'].change(fn=legacy_3d_mode, inputs=l_vars['depth_algorithm'], outputs=l_vars['midas_weight'])
|
| 50 |
+
l_vars['depth_algorithm'].change(fn=show_leres_html_msg, inputs=l_vars['depth_algorithm'], outputs=l_vars['leres_license_msg'])
|
| 51 |
+
l_vars['fps'].change(fn=change_gif_button_visibility, inputs=l_vars['fps'], outputs=l_vars['make_gif'])
|
| 52 |
+
l_vars['r_upscale_model'].change(fn=update_r_upscale_factor, inputs=l_vars['r_upscale_model'], outputs=l_vars['r_upscale_factor'])
|
| 53 |
+
l_vars['ncnn_upscale_model'].change(fn=update_r_upscale_factor, inputs=l_vars['ncnn_upscale_model'], outputs=l_vars['ncnn_upscale_factor'])
|
| 54 |
+
l_vars['ncnn_upscale_model'].change(update_upscale_out_res_by_model_name, inputs=[l_vars['ncnn_upscale_in_vid_res'], l_vars['ncnn_upscale_model']],
|
| 55 |
+
outputs=l_vars['ncnn_upscale_out_vid_res'])
|
| 56 |
+
l_vars['ncnn_upscale_factor'].change(update_upscale_out_res, inputs=[l_vars['ncnn_upscale_in_vid_res'], l_vars['ncnn_upscale_factor']], outputs=l_vars['ncnn_upscale_out_vid_res'])
|
| 57 |
+
l_vars['vid_to_upscale_chosen_file'].change(vid_upscale_gradio_update_stats, inputs=[l_vars['vid_to_upscale_chosen_file'], l_vars['ncnn_upscale_factor']],
|
| 58 |
+
outputs=[l_vars['ncnn_upscale_in_vid_fps_ui_window'], l_vars['ncnn_upscale_in_vid_frame_count_window'], l_vars['ncnn_upscale_in_vid_res'],
|
| 59 |
+
l_vars['ncnn_upscale_out_vid_res']])
|
| 60 |
+
l_vars['hybrid_comp_mask_type'].change(fn=hide_if_none, inputs=l_vars['hybrid_comp_mask_type'], outputs=l_vars['hybrid_comp_mask_row'])
|
| 61 |
+
hybrid_motion_outputs = [l_vars['hybrid_flow_method'], l_vars['hybrid_flow_factor_schedule'], l_vars['hybrid_flow_consistency'], l_vars['hybrid_consistency_blur'],
|
| 62 |
+
l_vars['hybrid_motion_use_prev_img']]
|
| 63 |
+
for output in hybrid_motion_outputs:
|
| 64 |
+
l_vars['hybrid_motion'].change(fn=disable_by_non_optical_flow, inputs=l_vars['hybrid_motion'], outputs=output)
|
| 65 |
+
l_vars['hybrid_flow_consistency'].change(fn=hide_if_false, inputs=l_vars['hybrid_flow_consistency'], outputs=l_vars['hybrid_consistency_blur'])
|
| 66 |
+
l_vars['hybrid_composite'].change(fn=disable_by_hybrid_composite_dynamic, inputs=[l_vars['hybrid_composite'], l_vars['hybrid_comp_mask_type']], outputs=l_vars['hybrid_comp_mask_row'])
|
| 67 |
+
hybrid_composite_outputs = [l_vars['humans_masking_accord'], l_vars['hybrid_sch_accord'], l_vars['hybrid_comp_mask_type'], l_vars['hybrid_use_first_frame_as_init_image'],
|
| 68 |
+
l_vars['hybrid_use_init_image']]
|
| 69 |
+
for output in hybrid_composite_outputs:
|
| 70 |
+
l_vars['hybrid_composite'].change(fn=hide_if_false, inputs=l_vars['hybrid_composite'], outputs=output)
|
| 71 |
+
hybrid_comp_mask_type_outputs = [l_vars['hybrid_comp_mask_blend_alpha_schedule_row'], l_vars['hybrid_comp_mask_contrast_schedule_row'],
|
| 72 |
+
l_vars['hybrid_comp_mask_auto_contrast_cutoff_high_schedule_row'],
|
| 73 |
+
l_vars['hybrid_comp_mask_auto_contrast_cutoff_low_schedule_row']]
|
| 74 |
+
for output in hybrid_comp_mask_type_outputs:
|
| 75 |
+
l_vars['hybrid_comp_mask_type'].change(fn=hide_if_none, inputs=l_vars['hybrid_comp_mask_type'], outputs=output)
|
| 76 |
+
# End of hybrid related
|
| 77 |
+
skip_video_creation_outputs = [l_vars['fps_out_format_row'], l_vars['soundtrack_row'], l_vars['store_frames_in_ram'], l_vars['make_gif'], l_vars['r_upscale_row'],
|
| 78 |
+
l_vars['delete_imgs']]
|
| 79 |
+
for output in skip_video_creation_outputs:
|
| 80 |
+
l_vars['skip_video_creation'].change(fn=change_visibility_from_skip_video, inputs=l_vars['skip_video_creation'], outputs=output)
|
| 81 |
+
l_vars['frame_interpolation_slow_mo_enabled'].change(fn=hide_if_false, inputs=l_vars['frame_interpolation_slow_mo_enabled'], outputs=l_vars['frame_interp_slow_mo_amount_column'])
|
| 82 |
+
l_vars['frame_interpolation_engine'].change(fn=change_interp_x_max_limit, inputs=[l_vars['frame_interpolation_engine'], l_vars['frame_interpolation_x_amount']],
|
| 83 |
+
outputs=l_vars['frame_interpolation_x_amount'])
|
| 84 |
+
# Populate the FPS and FCount values as soon as a video is uploaded to the FileUploadBox (vid_to_interpolate_chosen_file)
|
| 85 |
+
l_vars['vid_to_interpolate_chosen_file'].change(gradio_f_interp_get_fps_and_fcount,
|
| 86 |
+
inputs=[l_vars['vid_to_interpolate_chosen_file'], l_vars['frame_interpolation_x_amount'], l_vars['frame_interpolation_slow_mo_enabled'],
|
| 87 |
+
l_vars['frame_interpolation_slow_mo_amount']],
|
| 88 |
+
outputs=[l_vars['in_vid_fps_ui_window'], l_vars['in_vid_frame_count_window'], l_vars['out_interp_vid_estimated_fps']])
|
| 89 |
+
l_vars['vid_to_interpolate_chosen_file'].change(fn=hide_interp_stats, inputs=[l_vars['vid_to_interpolate_chosen_file']], outputs=[l_vars['interp_live_stats_row']])
|
| 90 |
+
interp_hide_list = [l_vars['frame_interpolation_slow_mo_enabled'], l_vars['frame_interpolation_keep_imgs'], l_vars['frame_interpolation_use_upscaled'], l_vars['frame_interp_amounts_row'], l_vars['interp_existing_video_row']]
|
| 91 |
+
for output in interp_hide_list:
|
| 92 |
+
l_vars['frame_interpolation_engine'].change(fn=hide_interp_by_interp_status, inputs=l_vars['frame_interpolation_engine'], outputs=output)
|
| 93 |
+
|
| 94 |
+
# START gradio-to-frame-interoplation/ upscaling functions
|
| 95 |
+
def upload_vid_to_interpolate(file, engine, x_am, sl_enabled, sl_am, keep_imgs, in_vid_fps):
|
| 96 |
+
# print msg and do nothing if vid not uploaded or interp_x not provided
|
| 97 |
+
if not file or engine == 'None':
|
| 98 |
+
return print("Please upload a video and set a proper value for 'Interp X'. Can't interpolate x0 times :)")
|
| 99 |
+
f_location, f_crf, f_preset = get_ffmpeg_params()
|
| 100 |
+
|
| 101 |
+
process_interp_vid_upload_logic(file, engine, x_am, sl_enabled, sl_am, keep_imgs, f_location, f_crf, f_preset, in_vid_fps, f_models_path, file.orig_name)
|
| 102 |
+
|
| 103 |
+
def upload_pics_to_interpolate(pic_list, engine, x_am, sl_enabled, sl_am, keep_imgs, fps, add_audio, audio_track):
|
| 104 |
+
from PIL import Image
|
| 105 |
+
|
| 106 |
+
if pic_list is None or len(pic_list) < 2:
|
| 107 |
+
return print("Please upload at least 2 pics for interpolation.")
|
| 108 |
+
f_location, f_crf, f_preset = get_ffmpeg_params()
|
| 109 |
+
# make sure all uploaded pics have the same resolution
|
| 110 |
+
pic_sizes = [Image.open(picture_path.name).size for picture_path in pic_list]
|
| 111 |
+
if len(set(pic_sizes)) != 1:
|
| 112 |
+
return print("All uploaded pics need to be of the same Width and Height / resolution.")
|
| 113 |
+
|
| 114 |
+
resolution = pic_sizes[0]
|
| 115 |
+
|
| 116 |
+
process_interp_pics_upload_logic(pic_list, engine, x_am, sl_enabled, sl_am, keep_imgs, f_location, f_crf, f_preset, fps, f_models_path, resolution, add_audio, audio_track)
|
| 117 |
+
|
| 118 |
+
def ncnn_upload_vid_to_upscale(vid_path, in_vid_fps, in_vid_res, out_vid_res, upscale_model, upscale_factor, keep_imgs):
|
| 119 |
+
if vid_path is None:
|
| 120 |
+
print("Please upload a video :)")
|
| 121 |
+
return
|
| 122 |
+
f_location, f_crf, f_preset = get_ffmpeg_params()
|
| 123 |
+
current_user = get_os()
|
| 124 |
+
process_ncnn_upscale_vid_upload_logic(vid_path, in_vid_fps, in_vid_res, out_vid_res, f_models_path, upscale_model, upscale_factor, keep_imgs, f_location, f_crf, f_preset, current_user)
|
| 125 |
+
|
| 126 |
+
def upload_vid_to_depth(vid_to_depth_chosen_file, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth, depth_keep_imgs):
|
| 127 |
+
# print msg and do nothing if vid not uploaded
|
| 128 |
+
if not vid_to_depth_chosen_file:
|
| 129 |
+
return print("Please upload a video :()")
|
| 130 |
+
f_location, f_crf, f_preset = get_ffmpeg_params()
|
| 131 |
+
|
| 132 |
+
process_depth_vid_upload_logic(vid_to_depth_chosen_file, mode, thresholding, threshold_value, threshold_value_max, adapt_block_size, adapt_c, invert, end_blur, midas_weight_vid2depth,
|
| 133 |
+
vid_to_depth_chosen_file.orig_name, depth_keep_imgs, f_location, f_crf, f_preset, f_models_path)
|
| 134 |
+
|
| 135 |
+
# END gradio-to-frame-interoplation/ upscaling functions
|
| 136 |
+
|
| 137 |
+
def change_visibility_from_skip_video(choice):
|
| 138 |
+
return gr.update(visible=False) if choice else gr.update(visible=True)
|
| 139 |
+
|
| 140 |
+
def update_r_upscale_factor(choice):
|
| 141 |
+
return gr.update(value='x4', choices=['x4']) if choice != 'realesr-animevideov3' else gr.update(value='x2', choices=['x2', 'x3', 'x4'])
|
| 142 |
+
|
| 143 |
+
def change_perlin_visibility(choice):
|
| 144 |
+
return gr.update(visible=choice == "perlin")
|
| 145 |
+
|
| 146 |
+
def legacy_3d_mode(choice):
|
| 147 |
+
return gr.update(visible=choice.lower() in ["midas+adabins (old)", 'zoe+adabins (old)'])
|
| 148 |
+
|
| 149 |
+
def change_color_coherence_image_path_visibility(choice):
|
| 150 |
+
return gr.update(visible=choice == "Image")
|
| 151 |
+
|
| 152 |
+
def change_color_coherence_video_every_N_frames_visibility(choice):
|
| 153 |
+
return gr.update(visible=choice == "Video Input")
|
| 154 |
+
|
| 155 |
+
def change_seed_iter_visibility(choice):
|
| 156 |
+
return gr.update(visible=choice == "iter")
|
| 157 |
+
|
| 158 |
+
def change_seed_schedule_visibility(choice):
|
| 159 |
+
return gr.update(visible=choice == "schedule")
|
| 160 |
+
|
| 161 |
+
def disable_pers_flip_accord(choice):
|
| 162 |
+
return gr.update(visible=True) if choice in ['2D', '3D'] else gr.update(visible=False)
|
| 163 |
+
|
| 164 |
+
def per_flip_handle(anim_mode, per_f_enabled):
|
| 165 |
+
if anim_mode in ['2D', '3D'] and per_f_enabled:
|
| 166 |
+
return gr.update(visible=True)
|
| 167 |
+
return gr.update(visible=False)
|
| 168 |
+
|
| 169 |
+
def change_max_frames_visibility(choice):
|
| 170 |
+
return gr.update(visible=choice != "Video Input")
|
| 171 |
+
|
| 172 |
+
def change_diffusion_cadence_visibility(choice):
|
| 173 |
+
return gr.update(visible=choice not in ['Video Input', 'Interpolation'])
|
| 174 |
+
|
| 175 |
+
def disble_3d_related_stuff(choice):
|
| 176 |
+
return gr.update(visible=False) if choice != '3D' else gr.update(visible=True)
|
| 177 |
+
|
| 178 |
+
def only_show_in_non_3d_mode(choice):
|
| 179 |
+
return gr.update(visible=False) if choice == '3D' else gr.update(visible=True)
|
| 180 |
+
|
| 181 |
+
def enable_2d_related_stuff(choice):
|
| 182 |
+
return gr.update(visible=True) if choice == '2D' else gr.update(visible=False)
|
| 183 |
+
|
| 184 |
+
def disable_by_interpolation(choice):
|
| 185 |
+
return gr.update(visible=False) if choice in ['Interpolation'] else gr.update(visible=True)
|
| 186 |
+
|
| 187 |
+
def disable_by_video_input(choice):
|
| 188 |
+
return gr.update(visible=False) if choice in ['Video Input'] else gr.update(visible=True)
|
| 189 |
+
|
| 190 |
+
def hide_if_none(choice):
|
| 191 |
+
return gr.update(visible=choice != "None")
|
| 192 |
+
|
| 193 |
+
def change_gif_button_visibility(choice):
|
| 194 |
+
if choice is None or choice == "":
|
| 195 |
+
return gr.update(visible=True)
|
| 196 |
+
return gr.update(visible=False, value=False) if int(choice) > 30 else gr.update(visible=True)
|
| 197 |
+
|
| 198 |
+
def hide_if_false(choice):
|
| 199 |
+
return gr.update(visible=True) if choice else gr.update(visible=False)
|
| 200 |
+
|
| 201 |
+
def hide_if_true(choice):
|
| 202 |
+
return gr.update(visible=False) if choice else gr.update(visible=True)
|
| 203 |
+
|
| 204 |
+
def disable_by_hybrid_composite_dynamic(choice, comp_mask_type):
|
| 205 |
+
if choice in ['Normal', 'Before Motion', 'After Generation']:
|
| 206 |
+
if comp_mask_type != 'None':
|
| 207 |
+
return gr.update(visible=True)
|
| 208 |
+
return gr.update(visible=False)
|
| 209 |
+
|
| 210 |
+
def disable_by_non_optical_flow(choice):
|
| 211 |
+
return gr.update(visible=False) if choice != 'Optical Flow' else gr.update(visible=True)
|
| 212 |
+
|
| 213 |
+
# Upscaling Gradio UI related funcs
|
| 214 |
+
def vid_upscale_gradio_update_stats(vid_path, upscale_factor):
|
| 215 |
+
if not vid_path:
|
| 216 |
+
return '---', '---', '---', '---'
|
| 217 |
+
factor = extract_number(upscale_factor)
|
| 218 |
+
fps, fcount, resolution = get_quick_vid_info(vid_path.name)
|
| 219 |
+
in_res_str = f"{resolution[0]}*{resolution[1]}"
|
| 220 |
+
out_res_str = f"{resolution[0] * factor}*{resolution[1] * factor}"
|
| 221 |
+
return fps, fcount, in_res_str, out_res_str
|
| 222 |
+
|
| 223 |
+
def update_upscale_out_res(in_res, upscale_factor):
|
| 224 |
+
if not in_res:
|
| 225 |
+
return '---'
|
| 226 |
+
factor = extract_number(upscale_factor)
|
| 227 |
+
w, h = [int(x) * factor for x in in_res.split('*')]
|
| 228 |
+
return f"{w}*{h}"
|
| 229 |
+
|
| 230 |
+
def update_upscale_out_res_by_model_name(in_res, upscale_model_name):
|
| 231 |
+
if not upscale_model_name or in_res == '---':
|
| 232 |
+
return '---'
|
| 233 |
+
factor = 2 if upscale_model_name == 'realesr-animevideov3' else 4
|
| 234 |
+
return f"{int(in_res.split('*')[0]) * factor}*{int(in_res.split('*')[1]) * factor}"
|
| 235 |
+
|
| 236 |
+
def hide_optical_flow_cadence(cadence_value):
|
| 237 |
+
return gr.update(visible=True) if cadence_value > 1 else gr.update(visible=False)
|
| 238 |
+
|
| 239 |
+
def hide_interp_by_interp_status(choice):
|
| 240 |
+
return gr.update(visible=False) if choice == 'None' else gr.update(visible=True)
|
| 241 |
+
|
| 242 |
+
def change_interp_x_max_limit(engine_name, current_value):
|
| 243 |
+
if engine_name == 'FILM':
|
| 244 |
+
return gr.update(maximum=300)
|
| 245 |
+
elif current_value > 10:
|
| 246 |
+
return gr.update(maximum=10, value=2)
|
| 247 |
+
return gr.update(maximum=10)
|
| 248 |
+
|
| 249 |
+
def hide_interp_stats(choice):
|
| 250 |
+
return gr.update(visible=True) if choice is not None else gr.update(visible=False)
|
| 251 |
+
|
| 252 |
+
def show_hybrid_html_msg(choice):
|
| 253 |
+
return gr.update(visible=True) if choice not in ['2D', '3D'] else gr.update(visible=False)
|
| 254 |
+
|
| 255 |
+
def change_hybrid_tab_status(choice):
|
| 256 |
+
return gr.update(visible=True) if choice in ['2D', '3D'] else gr.update(visible=False)
|
| 257 |
+
|
| 258 |
+
def show_leres_html_msg(choice):
|
| 259 |
+
return gr.update(visible=True) if choice.lower() == 'leres' else gr.update(visible=False)
|
| 260 |
+
|
| 261 |
+
def show_when_ddim(sampler_name):
|
| 262 |
+
return gr.update(visible=True) if sampler_name.lower() == 'ddim' else gr.update(visible=False)
|
| 263 |
+
|
| 264 |
+
def show_when_ancestral_samplers(sampler_name):
|
| 265 |
+
return gr.update(visible=True) if sampler_name.lower() in ['euler a', 'dpm++ 2s a', 'dpm2 a', 'dpm2 a karras', 'dpm++ 2s a karras'] else gr.update(visible=False)
|
| 266 |
+
|
| 267 |
+
def change_css(checkbox_status):
|
| 268 |
+
if checkbox_status:
|
| 269 |
+
display = "block"
|
| 270 |
+
else:
|
| 271 |
+
display = "none"
|
| 272 |
+
|
| 273 |
+
html_template = f'''
|
| 274 |
+
<style>
|
| 275 |
+
#tab_deforum_interface .svelte-e8n7p6, #f_interp_accord {{
|
| 276 |
+
display: {display} !important;
|
| 277 |
+
}}
|
| 278 |
+
</style>
|
| 279 |
+
'''
|
| 280 |
+
return html_template
|
scripts/deforum_helpers/human_masking.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, cv2
|
| 2 |
+
import torch
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from multiprocessing import freeze_support
|
| 5 |
+
|
| 6 |
+
def extract_frames(input_video_path, output_imgs_path):
|
| 7 |
+
# Open the video file
|
| 8 |
+
vidcap = cv2.VideoCapture(input_video_path)
|
| 9 |
+
|
| 10 |
+
# Get the total number of frames in the video
|
| 11 |
+
frame_count = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 12 |
+
|
| 13 |
+
# Create the output directory if it does not exist
|
| 14 |
+
os.makedirs(output_imgs_path, exist_ok=True)
|
| 15 |
+
|
| 16 |
+
# Extract the frames
|
| 17 |
+
for i in range(frame_count):
|
| 18 |
+
success, image = vidcap.read()
|
| 19 |
+
if success:
|
| 20 |
+
cv2.imwrite(os.path.join(output_imgs_path, f"frame{i}.png"), image)
|
| 21 |
+
print(f"{frame_count} frames extracted and saved to {output_imgs_path}")
|
| 22 |
+
|
| 23 |
+
def video2humanmasks(input_frames_path, output_folder_path, output_type, fps):
|
| 24 |
+
# freeze support is needed for video outputting
|
| 25 |
+
freeze_support()
|
| 26 |
+
|
| 27 |
+
# check if input path exists and is a directory
|
| 28 |
+
if not os.path.exists(input_frames_path) or not os.path.isdir(input_frames_path):
|
| 29 |
+
raise ValueError("Invalid input path: {}".format(input_frames_path))
|
| 30 |
+
|
| 31 |
+
# check if output path exists and is a directory
|
| 32 |
+
if not os.path.exists(output_folder_path) or not os.path.isdir(output_folder_path):
|
| 33 |
+
raise ValueError("Invalid output path: {}".format(output_folder_path))
|
| 34 |
+
|
| 35 |
+
# check if output_type is valid
|
| 36 |
+
valid_output_types = ["video", "pngs", "both"]
|
| 37 |
+
if output_type.lower() not in valid_output_types:
|
| 38 |
+
raise ValueError("Invalid output type: {}. Must be one of {}".format(output_type, valid_output_types))
|
| 39 |
+
|
| 40 |
+
# try to predict where torch cache lives, so we can try and fetch models from cache in the next step
|
| 41 |
+
predicted_torch_model_cache_path = os.path.join(Path.home(), ".cache", "torch", "hub", "hithereai_RobustVideoMatting_master")
|
| 42 |
+
predicted_rvm_cache_testilfe = os.path.join(predicted_torch_model_cache_path, "hubconf.py")
|
| 43 |
+
|
| 44 |
+
# try to fetch the models from cache, and only if it can't be find, download from the internet (to enable offline usage)
|
| 45 |
+
try:
|
| 46 |
+
# Try to fetch the models from cache
|
| 47 |
+
convert_video = torch.hub.load(predicted_torch_model_cache_path, "converter", source='local')
|
| 48 |
+
model = torch.hub.load(predicted_torch_model_cache_path, "resnet50", source='local').cuda()
|
| 49 |
+
except:
|
| 50 |
+
# Download from the internet if not found in cache
|
| 51 |
+
convert_video = torch.hub.load("hithereai/RobustVideoMatting", "converter")
|
| 52 |
+
model = torch.hub.load("hithereai/RobustVideoMatting", "resnet50").cuda()
|
| 53 |
+
|
| 54 |
+
output_alpha_vid_path = os.path.join(output_folder_path, "human_masked_video.mp4")
|
| 55 |
+
# extract humans masks from the input folder' imgs.
|
| 56 |
+
# in this step PNGs will be extracted only if output_type is set to PNGs. Otherwise a video will be made, and in the case of Both, the video will be extracted in the next step to PNGs
|
| 57 |
+
convert_video(
|
| 58 |
+
model,
|
| 59 |
+
input_source=input_frames_path, # full path of the folder that contains all of the extracted input imgs
|
| 60 |
+
output_type='video' if output_type.upper() in ("VIDEO", "BOTH") else 'png_sequence',
|
| 61 |
+
output_alpha=output_alpha_vid_path if output_type.upper() in ("VIDEO", "BOTH") else output_folder_path,
|
| 62 |
+
output_video_mbps=4,
|
| 63 |
+
output_video_fps=fps,
|
| 64 |
+
downsample_ratio=None, # None for auto
|
| 65 |
+
seq_chunk=12, # Process n frames at once for better parallelism
|
| 66 |
+
progress=True # show extraction progress
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
if output_type.lower() == "both":
|
| 70 |
+
extract_frames(output_alpha_vid_path, output_folder_path)
|
scripts/deforum_helpers/hybrid_video.py
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pathlib
|
| 3 |
+
import random
|
| 4 |
+
import cv2
|
| 5 |
+
import numpy as np
|
| 6 |
+
import PIL
|
| 7 |
+
from PIL import Image, ImageChops, ImageOps, ImageEnhance
|
| 8 |
+
from scipy.ndimage.filters import gaussian_filter
|
| 9 |
+
from .consistency_check import make_consistency
|
| 10 |
+
from .human_masking import video2humanmasks
|
| 11 |
+
from .load_images import load_image
|
| 12 |
+
from .video_audio_utilities import vid2frames, get_quick_vid_info, get_frame_name
|
| 13 |
+
|
| 14 |
+
def delete_all_imgs_in_folder(folder_path):
|
| 15 |
+
files = list(pathlib.Path(folder_path).glob('*.jpg'))
|
| 16 |
+
files.extend(list(pathlib.Path(folder_path).glob('*.png')))
|
| 17 |
+
for f in files: os.remove(f)
|
| 18 |
+
|
| 19 |
+
def hybrid_generation(args, anim_args, root):
|
| 20 |
+
video_in_frame_path = os.path.join(args.outdir, 'inputframes')
|
| 21 |
+
hybrid_frame_path = os.path.join(args.outdir, 'hybridframes')
|
| 22 |
+
human_masks_path = os.path.join(args.outdir, 'human_masks')
|
| 23 |
+
|
| 24 |
+
# create hybridframes folder whether using init_image or inputframes
|
| 25 |
+
os.makedirs(hybrid_frame_path, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
if anim_args.hybrid_generate_inputframes:
|
| 28 |
+
# create folders for the video input frames and optional hybrid frames to live in
|
| 29 |
+
os.makedirs(video_in_frame_path, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
# delete frames if overwrite = true
|
| 32 |
+
if anim_args.overwrite_extracted_frames:
|
| 33 |
+
delete_all_imgs_in_folder(hybrid_frame_path)
|
| 34 |
+
|
| 35 |
+
# save the video frames from input video
|
| 36 |
+
print(f"Video to extract: {anim_args.video_init_path}")
|
| 37 |
+
print(f"Extracting video (1 every {anim_args.extract_nth_frame}) frames to {video_in_frame_path}...")
|
| 38 |
+
video_fps = vid2frames(video_path=anim_args.video_init_path, video_in_frame_path=video_in_frame_path, n=anim_args.extract_nth_frame, overwrite=anim_args.overwrite_extracted_frames, extract_from_frame=anim_args.extract_from_frame, extract_to_frame=anim_args.extract_to_frame)
|
| 39 |
+
|
| 40 |
+
# extract alpha masks of humans from the extracted input video imgs
|
| 41 |
+
if anim_args.hybrid_generate_human_masks != "None":
|
| 42 |
+
# create a folder for the human masks imgs to live in
|
| 43 |
+
print(f"Checking /creating a folder for the human masks")
|
| 44 |
+
os.makedirs(human_masks_path, exist_ok=True)
|
| 45 |
+
|
| 46 |
+
# delete frames if overwrite = true
|
| 47 |
+
if anim_args.overwrite_extracted_frames:
|
| 48 |
+
delete_all_imgs_in_folder(human_masks_path)
|
| 49 |
+
|
| 50 |
+
# in case that generate_input_frames isn't selected, we won't get the video fps rate as vid2frames isn't called, So we'll check the video fps in here instead
|
| 51 |
+
if not anim_args.hybrid_generate_inputframes:
|
| 52 |
+
_, video_fps, _ = get_quick_vid_info(anim_args.video_init_path)
|
| 53 |
+
|
| 54 |
+
# calculate the correct fps of the masked video according to the original video fps and 'extract_nth_frame'
|
| 55 |
+
output_fps = video_fps/anim_args.extract_nth_frame
|
| 56 |
+
|
| 57 |
+
# generate the actual alpha masks from the input imgs
|
| 58 |
+
print(f"Extracting alpha humans masks from the input frames")
|
| 59 |
+
video2humanmasks(video_in_frame_path, human_masks_path, anim_args.hybrid_generate_human_masks, output_fps)
|
| 60 |
+
|
| 61 |
+
# get sorted list of inputfiles
|
| 62 |
+
inputfiles = sorted(pathlib.Path(video_in_frame_path).glob('*.jpg'))
|
| 63 |
+
|
| 64 |
+
if not anim_args.hybrid_use_init_image:
|
| 65 |
+
# determine max frames from length of input frames
|
| 66 |
+
anim_args.max_frames = len(inputfiles)
|
| 67 |
+
print(f"Using {anim_args.max_frames} input frames from {video_in_frame_path}...")
|
| 68 |
+
|
| 69 |
+
# use first frame as init
|
| 70 |
+
if anim_args.hybrid_use_first_frame_as_init_image:
|
| 71 |
+
for f in inputfiles:
|
| 72 |
+
args.init_image = str(f)
|
| 73 |
+
args.use_init = True
|
| 74 |
+
print(f"Using init_image from video: {args.init_image}")
|
| 75 |
+
break
|
| 76 |
+
|
| 77 |
+
return args, anim_args, inputfiles
|
| 78 |
+
|
| 79 |
+
def hybrid_composite(args, anim_args, frame_idx, prev_img, depth_model, hybrid_comp_schedules, root):
|
| 80 |
+
video_frame = os.path.join(args.outdir, 'inputframes', get_frame_name(anim_args.video_init_path) + f"{frame_idx:09}.jpg")
|
| 81 |
+
video_depth_frame = os.path.join(args.outdir, 'hybridframes', get_frame_name(anim_args.video_init_path) + f"_vid_depth{frame_idx:09}.jpg")
|
| 82 |
+
depth_frame = os.path.join(args.outdir, f"{root.timestring}_depth_{frame_idx-1:09}.png")
|
| 83 |
+
mask_frame = os.path.join(args.outdir, 'hybridframes', get_frame_name(anim_args.video_init_path) + f"_mask{frame_idx:09}.jpg")
|
| 84 |
+
comp_frame = os.path.join(args.outdir, 'hybridframes', get_frame_name(anim_args.video_init_path) + f"_comp{frame_idx:09}.jpg")
|
| 85 |
+
prev_frame = os.path.join(args.outdir, 'hybridframes', get_frame_name(anim_args.video_init_path) + f"_prev{frame_idx:09}.jpg")
|
| 86 |
+
prev_img = cv2.cvtColor(prev_img, cv2.COLOR_BGR2RGB)
|
| 87 |
+
prev_img_hybrid = Image.fromarray(prev_img)
|
| 88 |
+
if anim_args.hybrid_use_init_image:
|
| 89 |
+
video_image = load_image(args.init_image)
|
| 90 |
+
else:
|
| 91 |
+
video_image = Image.open(video_frame)
|
| 92 |
+
video_image = video_image.resize((args.W, args.H), PIL.Image.LANCZOS)
|
| 93 |
+
hybrid_mask = None
|
| 94 |
+
|
| 95 |
+
# composite mask types
|
| 96 |
+
if anim_args.hybrid_comp_mask_type == 'Depth': # get depth from last generation
|
| 97 |
+
hybrid_mask = Image.open(depth_frame)
|
| 98 |
+
elif anim_args.hybrid_comp_mask_type == 'Video Depth': # get video depth
|
| 99 |
+
video_depth = depth_model.predict(np.array(video_image), anim_args.midas_weight, root.half_precision)
|
| 100 |
+
depth_model.save(video_depth_frame, video_depth)
|
| 101 |
+
hybrid_mask = Image.open(video_depth_frame)
|
| 102 |
+
elif anim_args.hybrid_comp_mask_type == 'Blend': # create blend mask image
|
| 103 |
+
hybrid_mask = Image.blend(ImageOps.grayscale(prev_img_hybrid), ImageOps.grayscale(video_image), hybrid_comp_schedules['mask_blend_alpha'])
|
| 104 |
+
elif anim_args.hybrid_comp_mask_type == 'Difference': # create difference mask image
|
| 105 |
+
hybrid_mask = ImageChops.difference(ImageOps.grayscale(prev_img_hybrid), ImageOps.grayscale(video_image))
|
| 106 |
+
|
| 107 |
+
# optionally invert mask, if mask type is defined
|
| 108 |
+
if anim_args.hybrid_comp_mask_inverse and anim_args.hybrid_comp_mask_type != "None":
|
| 109 |
+
hybrid_mask = ImageOps.invert(hybrid_mask)
|
| 110 |
+
|
| 111 |
+
# if a mask type is selected, make composition
|
| 112 |
+
if hybrid_mask is None:
|
| 113 |
+
hybrid_comp = video_image
|
| 114 |
+
else:
|
| 115 |
+
# ensure grayscale
|
| 116 |
+
hybrid_mask = ImageOps.grayscale(hybrid_mask)
|
| 117 |
+
# equalization before
|
| 118 |
+
if anim_args.hybrid_comp_mask_equalize in ['Before', 'Both']:
|
| 119 |
+
hybrid_mask = ImageOps.equalize(hybrid_mask)
|
| 120 |
+
# contrast
|
| 121 |
+
hybrid_mask = ImageEnhance.Contrast(hybrid_mask).enhance(hybrid_comp_schedules['mask_contrast'])
|
| 122 |
+
# auto contrast with cutoffs lo/hi
|
| 123 |
+
if anim_args.hybrid_comp_mask_auto_contrast:
|
| 124 |
+
hybrid_mask = autocontrast_grayscale(np.array(hybrid_mask), hybrid_comp_schedules['mask_auto_contrast_cutoff_low'], hybrid_comp_schedules['mask_auto_contrast_cutoff_high'])
|
| 125 |
+
hybrid_mask = Image.fromarray(hybrid_mask)
|
| 126 |
+
hybrid_mask = ImageOps.grayscale(hybrid_mask)
|
| 127 |
+
if anim_args.hybrid_comp_save_extra_frames:
|
| 128 |
+
hybrid_mask.save(mask_frame)
|
| 129 |
+
# equalization after
|
| 130 |
+
if anim_args.hybrid_comp_mask_equalize in ['After', 'Both']:
|
| 131 |
+
hybrid_mask = ImageOps.equalize(hybrid_mask)
|
| 132 |
+
# do compositing and save
|
| 133 |
+
hybrid_comp = Image.composite(prev_img_hybrid, video_image, hybrid_mask)
|
| 134 |
+
if anim_args.hybrid_comp_save_extra_frames:
|
| 135 |
+
hybrid_comp.save(comp_frame)
|
| 136 |
+
|
| 137 |
+
# final blend of composite with prev_img, or just a blend if no composite is selected
|
| 138 |
+
hybrid_blend = Image.blend(prev_img_hybrid, hybrid_comp, hybrid_comp_schedules['alpha'])
|
| 139 |
+
if anim_args.hybrid_comp_save_extra_frames:
|
| 140 |
+
hybrid_blend.save(prev_frame)
|
| 141 |
+
|
| 142 |
+
prev_img = cv2.cvtColor(np.array(hybrid_blend), cv2.COLOR_RGB2BGR)
|
| 143 |
+
|
| 144 |
+
# restore to np array and return
|
| 145 |
+
return args, prev_img
|
| 146 |
+
|
| 147 |
+
def get_matrix_for_hybrid_motion(frame_idx, dimensions, inputfiles, hybrid_motion):
|
| 148 |
+
print(f"Calculating {hybrid_motion} RANSAC matrix for frames {frame_idx} to {frame_idx+1}")
|
| 149 |
+
img1 = cv2.cvtColor(get_resized_image_from_filename(str(inputfiles[frame_idx]), dimensions), cv2.COLOR_BGR2GRAY)
|
| 150 |
+
img2 = cv2.cvtColor(get_resized_image_from_filename(str(inputfiles[frame_idx+1]), dimensions), cv2.COLOR_BGR2GRAY)
|
| 151 |
+
M = get_transformation_matrix_from_images(img1, img2, hybrid_motion)
|
| 152 |
+
return M
|
| 153 |
+
|
| 154 |
+
def get_matrix_for_hybrid_motion_prev(frame_idx, dimensions, inputfiles, prev_img, hybrid_motion):
|
| 155 |
+
print(f"Calculating {hybrid_motion} RANSAC matrix for frames {frame_idx} to {frame_idx+1}")
|
| 156 |
+
# first handle invalid images by returning default matrix
|
| 157 |
+
height, width = prev_img.shape[:2]
|
| 158 |
+
if height == 0 or width == 0 or prev_img != np.uint8:
|
| 159 |
+
return get_hybrid_motion_default_matrix(hybrid_motion)
|
| 160 |
+
else:
|
| 161 |
+
prev_img_gray = cv2.cvtColor(prev_img, cv2.COLOR_BGR2GRAY)
|
| 162 |
+
img = cv2.cvtColor(get_resized_image_from_filename(str(inputfiles[frame_idx+1]), dimensions), cv2.COLOR_BGR2GRAY)
|
| 163 |
+
M = get_transformation_matrix_from_images(prev_img_gray, img, hybrid_motion)
|
| 164 |
+
return M
|
| 165 |
+
|
| 166 |
+
def get_flow_for_hybrid_motion(frame_idx, dimensions, inputfiles, hybrid_frame_path, prev_flow, method, raft_model, consistency_check=True, consistency_blur=0, do_flow_visualization=False):
|
| 167 |
+
print(f"Calculating {method} optical flow {'w/consistency mask' if consistency_check else ''} for frames {frame_idx} to {frame_idx+1}")
|
| 168 |
+
i1 = get_resized_image_from_filename(str(inputfiles[frame_idx]), dimensions)
|
| 169 |
+
i2 = get_resized_image_from_filename(str(inputfiles[frame_idx+1]), dimensions)
|
| 170 |
+
if consistency_check:
|
| 171 |
+
flow, reliable_flow = get_reliable_flow_from_images(i1, i2, method, raft_model, prev_flow, consistency_blur) # forward flow w/backward consistency check
|
| 172 |
+
if do_flow_visualization: save_flow_mask_visualization(frame_idx, reliable_flow, hybrid_frame_path)
|
| 173 |
+
else:
|
| 174 |
+
flow = get_flow_from_images(i1, i2, method, raft_model, prev_flow) # old single flow forward
|
| 175 |
+
if do_flow_visualization: save_flow_visualization(frame_idx, dimensions, flow, inputfiles, hybrid_frame_path)
|
| 176 |
+
return flow
|
| 177 |
+
|
| 178 |
+
def get_flow_for_hybrid_motion_prev(frame_idx, dimensions, inputfiles, hybrid_frame_path, prev_flow, prev_img, method, raft_model, consistency_check=True, consistency_blur=0, do_flow_visualization=False):
|
| 179 |
+
print(f"Calculating {method} optical flow {'w/consistency mask' if consistency_check else ''} for frames {frame_idx} to {frame_idx+1}")
|
| 180 |
+
reliable_flow = None
|
| 181 |
+
# first handle invalid images by returning default flow
|
| 182 |
+
height, width = prev_img.shape[:2]
|
| 183 |
+
if height == 0 or width == 0:
|
| 184 |
+
flow = get_hybrid_motion_default_flow(dimensions)
|
| 185 |
+
else:
|
| 186 |
+
i1 = prev_img.astype(np.uint8)
|
| 187 |
+
i2 = get_resized_image_from_filename(str(inputfiles[frame_idx+1]), dimensions)
|
| 188 |
+
if consistency_check:
|
| 189 |
+
flow, reliable_flow = get_reliable_flow_from_images(i1, i2, method, raft_model, prev_flow, consistency_blur) # forward flow w/backward consistency check
|
| 190 |
+
if do_flow_visualization: save_flow_mask_visualization(frame_idx, reliable_flow, hybrid_frame_path)
|
| 191 |
+
else:
|
| 192 |
+
flow = get_flow_from_images(i1, i2, method, raft_model, prev_flow)
|
| 193 |
+
if do_flow_visualization: save_flow_visualization(frame_idx, dimensions, flow, inputfiles, hybrid_frame_path)
|
| 194 |
+
return flow
|
| 195 |
+
|
| 196 |
+
def get_reliable_flow_from_images(i1, i2, method, raft_model, prev_flow, consistency_blur, reliability=0):
|
| 197 |
+
flow_forward = get_flow_from_images(i1, i2, method, raft_model, prev_flow)
|
| 198 |
+
flow_backward = get_flow_from_images(i2, i1, method, raft_model, None)
|
| 199 |
+
reliable_flow = make_consistency(flow_forward, flow_backward, edges_unreliable=False)
|
| 200 |
+
if consistency_blur > 0:
|
| 201 |
+
reliable_flow = custom_gaussian_blur(reliable_flow.astype(np.float32), 1, consistency_blur)
|
| 202 |
+
return filter_flow(flow_forward, reliable_flow, consistency_blur, reliability), reliable_flow
|
| 203 |
+
|
| 204 |
+
def custom_gaussian_blur(input_array, blur_size, sigma):
|
| 205 |
+
return gaussian_filter(input_array, sigma=(sigma, sigma, 0), order=0, mode='constant', cval=0.0, truncate=blur_size)
|
| 206 |
+
|
| 207 |
+
def filter_flow(flow, reliable_flow, reliability=0.5, consistency_blur=0):
|
| 208 |
+
# reliability from reliabile flow: -0.75 is bad, 0 is meh/outside, 1 is great
|
| 209 |
+
# Create a mask from the first channel of the reliable_flow array
|
| 210 |
+
mask = reliable_flow[..., 0]
|
| 211 |
+
|
| 212 |
+
# to set everything to 1 or 0 based on reliability
|
| 213 |
+
# mask = np.where(mask >= reliability, 1, 0)
|
| 214 |
+
|
| 215 |
+
# Expand the mask to match the shape of the forward_flow array
|
| 216 |
+
mask = np.repeat(mask[..., np.newaxis], flow.shape[2], axis=2)
|
| 217 |
+
|
| 218 |
+
# Apply the mask to the flow
|
| 219 |
+
return flow * mask
|
| 220 |
+
|
| 221 |
+
def image_transform_ransac(image_cv2, M, hybrid_motion, depth=None):
|
| 222 |
+
if hybrid_motion == "Perspective":
|
| 223 |
+
return image_transform_perspective(image_cv2, M, depth)
|
| 224 |
+
else: # Affine
|
| 225 |
+
return image_transform_affine(image_cv2, M, depth)
|
| 226 |
+
|
| 227 |
+
def image_transform_optical_flow(img, flow, flow_factor):
|
| 228 |
+
# if flow factor not normal, calculate flow factor
|
| 229 |
+
if flow_factor != 1:
|
| 230 |
+
flow = flow * flow_factor
|
| 231 |
+
# flow is reversed, so you need to reverse it:
|
| 232 |
+
flow = -flow
|
| 233 |
+
h, w = img.shape[:2]
|
| 234 |
+
flow[:, :, 0] += np.arange(w)
|
| 235 |
+
flow[:, :, 1] += np.arange(h)[:,np.newaxis]
|
| 236 |
+
return remap(img, flow)
|
| 237 |
+
|
| 238 |
+
def image_transform_affine(image_cv2, M, depth=None):
|
| 239 |
+
if depth is None:
|
| 240 |
+
return cv2.warpAffine(
|
| 241 |
+
image_cv2,
|
| 242 |
+
M,
|
| 243 |
+
(image_cv2.shape[1],image_cv2.shape[0]),
|
| 244 |
+
borderMode=cv2.BORDER_REFLECT_101
|
| 245 |
+
)
|
| 246 |
+
else: # NEED TO IMPLEMENT THE FOLLOWING FUNCTION
|
| 247 |
+
return depth_based_affine_warp(
|
| 248 |
+
image_cv2,
|
| 249 |
+
depth,
|
| 250 |
+
M
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
def image_transform_perspective(image_cv2, M, depth=None):
|
| 254 |
+
if depth is None:
|
| 255 |
+
return cv2.warpPerspective(
|
| 256 |
+
image_cv2,
|
| 257 |
+
M,
|
| 258 |
+
(image_cv2.shape[1], image_cv2.shape[0]),
|
| 259 |
+
borderMode=cv2.BORDER_REFLECT_101
|
| 260 |
+
)
|
| 261 |
+
else: # NEED TO IMPLEMENT THE FOLLOWING FUNCTION
|
| 262 |
+
return render_3d_perspective(
|
| 263 |
+
image_cv2,
|
| 264 |
+
depth,
|
| 265 |
+
M
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
def get_hybrid_motion_default_matrix(hybrid_motion):
|
| 269 |
+
if hybrid_motion == "Perspective":
|
| 270 |
+
arr = np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
|
| 271 |
+
else:
|
| 272 |
+
arr = np.array([[1., 0., 0.], [0., 1., 0.]])
|
| 273 |
+
return arr
|
| 274 |
+
|
| 275 |
+
def get_hybrid_motion_default_flow(dimensions):
|
| 276 |
+
cols, rows = dimensions
|
| 277 |
+
flow = np.zeros((rows, cols, 2), np.float32)
|
| 278 |
+
return flow
|
| 279 |
+
|
| 280 |
+
def get_transformation_matrix_from_images(img1, img2, hybrid_motion, confidence=0.75):
|
| 281 |
+
# Create SIFT detector and feature extractor
|
| 282 |
+
sift = cv2.SIFT_create()
|
| 283 |
+
|
| 284 |
+
# Detect keypoints and compute descriptors
|
| 285 |
+
kp1, des1 = sift.detectAndCompute(img1, None)
|
| 286 |
+
kp2, des2 = sift.detectAndCompute(img2, None)
|
| 287 |
+
|
| 288 |
+
# Create BFMatcher object and match descriptors
|
| 289 |
+
bf = cv2.BFMatcher()
|
| 290 |
+
matches = bf.knnMatch(des1, des2, k=2)
|
| 291 |
+
|
| 292 |
+
# Apply ratio test to filter good matches
|
| 293 |
+
good_matches = []
|
| 294 |
+
for m, n in matches:
|
| 295 |
+
if m.distance < confidence * n.distance:
|
| 296 |
+
good_matches.append(m)
|
| 297 |
+
|
| 298 |
+
if len(good_matches) <= 8:
|
| 299 |
+
get_hybrid_motion_default_matrix(hybrid_motion)
|
| 300 |
+
|
| 301 |
+
# Convert keypoints to numpy arrays
|
| 302 |
+
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 303 |
+
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
|
| 304 |
+
|
| 305 |
+
if len(src_pts) <= 8 or len(dst_pts) <= 8:
|
| 306 |
+
return get_hybrid_motion_default_matrix(hybrid_motion)
|
| 307 |
+
elif hybrid_motion == "Perspective": # Perspective transformation (3x3)
|
| 308 |
+
transformation_matrix, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
|
| 309 |
+
return transformation_matrix
|
| 310 |
+
else: # Affine - rigid transformation (no skew 3x2)
|
| 311 |
+
transformation_rigid_matrix, rigid_mask = cv2.estimateAffinePartial2D(src_pts, dst_pts)
|
| 312 |
+
return transformation_rigid_matrix
|
| 313 |
+
|
| 314 |
+
def get_flow_from_images(i1, i2, method, raft_model, prev_flow=None):
|
| 315 |
+
if method == "RAFT":
|
| 316 |
+
if raft_model is None:
|
| 317 |
+
raise Exception("RAFT Model not provided to get_flow_from_images function, cannot continue.")
|
| 318 |
+
return get_flow_from_images_RAFT(i1, i2, raft_model)
|
| 319 |
+
elif method == "DIS Medium":
|
| 320 |
+
return get_flow_from_images_DIS(i1, i2, 'medium', prev_flow)
|
| 321 |
+
elif method == "DIS Fine":
|
| 322 |
+
return get_flow_from_images_DIS(i1, i2, 'fine', prev_flow)
|
| 323 |
+
elif method == "DenseRLOF": # Unused - requires running opencv-contrib-python (full opencv) INSTEAD of opencv-python
|
| 324 |
+
return get_flow_from_images_Dense_RLOF(i1, i2, prev_flow)
|
| 325 |
+
elif method == "SF": # Unused - requires running opencv-contrib-python (full opencv) INSTEAD of opencv-python
|
| 326 |
+
return get_flow_from_images_SF(i1, i2, prev_flow)
|
| 327 |
+
elif method == "DualTVL1": # Unused - requires running opencv-contrib-python (full opencv) INSTEAD of opencv-python
|
| 328 |
+
return get_flow_from_images_DualTVL1(i1, i2, prev_flow)
|
| 329 |
+
elif method == "DeepFlow": # Unused - requires running opencv-contrib-python (full opencv) INSTEAD of opencv-python
|
| 330 |
+
return get_flow_from_images_DeepFlow(i1, i2, prev_flow)
|
| 331 |
+
elif method == "PCAFlow": # Unused - requires running opencv-contrib-python (full opencv) INSTEAD of opencv-python
|
| 332 |
+
return get_flow_from_images_PCAFlow(i1, i2, prev_flow)
|
| 333 |
+
elif method == "Farneback": # Farneback Normal:
|
| 334 |
+
return get_flow_from_images_Farneback(i1, i2, prev_flow)
|
| 335 |
+
# if we reached this point, something went wrong. raise an error:
|
| 336 |
+
raise RuntimeError(f"Invald flow method name: '{method}'")
|
| 337 |
+
|
| 338 |
+
def get_flow_from_images_RAFT(i1, i2, raft_model):
|
| 339 |
+
flow = raft_model.predict(i1, i2)
|
| 340 |
+
return flow
|
| 341 |
+
|
| 342 |
+
def get_flow_from_images_DIS(i1, i2, preset, prev_flow):
|
| 343 |
+
# DIS PRESETS CHART KEY: finest scale, grad desc its, patch size
|
| 344 |
+
# DIS_MEDIUM: 1, 25, 8 | DIS_FAST: 2, 16, 8 | DIS_ULTRAFAST: 2, 12, 8
|
| 345 |
+
if preset == 'medium': preset_code = cv2.DISOPTICAL_FLOW_PRESET_MEDIUM
|
| 346 |
+
elif preset == 'fast': preset_code = cv2.DISOPTICAL_FLOW_PRESET_FAST
|
| 347 |
+
elif preset == 'ultrafast': preset_code = cv2.DISOPTICAL_FLOW_PRESET_ULTRAFAST
|
| 348 |
+
elif preset in ['slow','fine']: preset_code = None
|
| 349 |
+
i1 = cv2.cvtColor(i1, cv2.COLOR_BGR2GRAY)
|
| 350 |
+
i2 = cv2.cvtColor(i2, cv2.COLOR_BGR2GRAY)
|
| 351 |
+
dis = cv2.DISOpticalFlow_create(preset_code)
|
| 352 |
+
# custom presets
|
| 353 |
+
if preset == 'slow':
|
| 354 |
+
dis.setGradientDescentIterations(192)
|
| 355 |
+
dis.setFinestScale(1)
|
| 356 |
+
dis.setPatchSize(8)
|
| 357 |
+
dis.setPatchStride(4)
|
| 358 |
+
if preset == 'fine':
|
| 359 |
+
dis.setGradientDescentIterations(192)
|
| 360 |
+
dis.setFinestScale(0)
|
| 361 |
+
dis.setPatchSize(8)
|
| 362 |
+
dis.setPatchStride(4)
|
| 363 |
+
return dis.calc(i1, i2, prev_flow)
|
| 364 |
+
|
| 365 |
+
def get_flow_from_images_Dense_RLOF(i1, i2, last_flow=None):
|
| 366 |
+
return cv2.optflow.calcOpticalFlowDenseRLOF(i1, i2, flow = last_flow)
|
| 367 |
+
|
| 368 |
+
def get_flow_from_images_SF(i1, i2, last_flow=None, layers = 3, averaging_block_size = 2, max_flow = 4):
|
| 369 |
+
return cv2.optflow.calcOpticalFlowSF(i1, i2, layers, averaging_block_size, max_flow)
|
| 370 |
+
|
| 371 |
+
def get_flow_from_images_DualTVL1(i1, i2, prev_flow):
|
| 372 |
+
i1 = cv2.cvtColor(i1, cv2.COLOR_BGR2GRAY)
|
| 373 |
+
i2 = cv2.cvtColor(i2, cv2.COLOR_BGR2GRAY)
|
| 374 |
+
f = cv2.optflow.DualTVL1OpticalFlow_create()
|
| 375 |
+
return f.calc(i1, i2, prev_flow)
|
| 376 |
+
|
| 377 |
+
def get_flow_from_images_DeepFlow(i1, i2, prev_flow):
|
| 378 |
+
i1 = cv2.cvtColor(i1, cv2.COLOR_BGR2GRAY)
|
| 379 |
+
i2 = cv2.cvtColor(i2, cv2.COLOR_BGR2GRAY)
|
| 380 |
+
f = cv2.optflow.createOptFlow_DeepFlow()
|
| 381 |
+
return f.calc(i1, i2, prev_flow)
|
| 382 |
+
|
| 383 |
+
def get_flow_from_images_PCAFlow(i1, i2, prev_flow):
|
| 384 |
+
i1 = cv2.cvtColor(i1, cv2.COLOR_BGR2GRAY)
|
| 385 |
+
i2 = cv2.cvtColor(i2, cv2.COLOR_BGR2GRAY)
|
| 386 |
+
f = cv2.optflow.createOptFlow_PCAFlow()
|
| 387 |
+
return f.calc(i1, i2, prev_flow)
|
| 388 |
+
|
| 389 |
+
def get_flow_from_images_Farneback(i1, i2, preset="normal", last_flow=None, pyr_scale = 0.5, levels = 3, winsize = 15, iterations = 3, poly_n = 5, poly_sigma = 1.2, flags = 0):
|
| 390 |
+
flags = cv2.OPTFLOW_FARNEBACK_GAUSSIAN # Specify the operation flags
|
| 391 |
+
pyr_scale = 0.5 # The image scale (<1) to build pyramids for each image
|
| 392 |
+
if preset == "fine":
|
| 393 |
+
levels = 13 # The number of pyramid layers, including the initial image
|
| 394 |
+
winsize = 77 # The averaging window size
|
| 395 |
+
iterations = 13 # The number of iterations at each pyramid level
|
| 396 |
+
poly_n = 15 # The size of the pixel neighborhood used to find polynomial expansion in each pixel
|
| 397 |
+
poly_sigma = 0.8 # The standard deviation of the Gaussian used to smooth derivatives used as a basis for the polynomial expansion
|
| 398 |
+
else: # "normal"
|
| 399 |
+
levels = 5 # The number of pyramid layers, including the initial image
|
| 400 |
+
winsize = 21 # The averaging window size
|
| 401 |
+
iterations = 5 # The number of iterations at each pyramid level
|
| 402 |
+
poly_n = 7 # The size of the pixel neighborhood used to find polynomial expansion in each pixel
|
| 403 |
+
poly_sigma = 1.2 # The standard deviation of the Gaussian used to smooth derivatives used as a basis for the polynomial expansion
|
| 404 |
+
i1 = cv2.cvtColor(i1, cv2.COLOR_BGR2GRAY)
|
| 405 |
+
i2 = cv2.cvtColor(i2, cv2.COLOR_BGR2GRAY)
|
| 406 |
+
flags = 0 # flags = cv2.OPTFLOW_USE_INITIAL_FLOW
|
| 407 |
+
flow = cv2.calcOpticalFlowFarneback(i1, i2, last_flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)
|
| 408 |
+
return flow
|
| 409 |
+
|
| 410 |
+
def save_flow_visualization(frame_idx, dimensions, flow, inputfiles, hybrid_frame_path):
|
| 411 |
+
flow_img_file = os.path.join(hybrid_frame_path, f"flow{frame_idx:09}.jpg")
|
| 412 |
+
flow_img = cv2.imread(str(inputfiles[frame_idx]))
|
| 413 |
+
flow_img = cv2.resize(flow_img, (dimensions[0], dimensions[1]), cv2.INTER_AREA)
|
| 414 |
+
flow_img = cv2.cvtColor(flow_img, cv2.COLOR_RGB2GRAY)
|
| 415 |
+
flow_img = cv2.cvtColor(flow_img, cv2.COLOR_GRAY2BGR)
|
| 416 |
+
flow_img = draw_flow_lines_in_grid_in_color(flow_img, flow)
|
| 417 |
+
flow_img = cv2.cvtColor(flow_img, cv2.COLOR_BGR2RGB)
|
| 418 |
+
cv2.imwrite(flow_img_file, flow_img)
|
| 419 |
+
print(f"Saved optical flow visualization: {flow_img_file}")
|
| 420 |
+
|
| 421 |
+
def save_flow_mask_visualization(frame_idx, reliable_flow, hybrid_frame_path, color=True):
|
| 422 |
+
flow_mask_img_file = os.path.join(hybrid_frame_path, f"flow_mask{frame_idx:09}.jpg")
|
| 423 |
+
if color:
|
| 424 |
+
# Normalize the reliable_flow array to the range [0, 255]
|
| 425 |
+
normalized_reliable_flow = (reliable_flow - reliable_flow.min()) / (reliable_flow.max() - reliable_flow.min()) * 255
|
| 426 |
+
# Change the data type to np.uint8
|
| 427 |
+
mask_image = normalized_reliable_flow.astype(np.uint8)
|
| 428 |
+
else:
|
| 429 |
+
# Extract the first channel of the reliable_flow array
|
| 430 |
+
first_channel = reliable_flow[..., 0]
|
| 431 |
+
# Normalize the first channel to the range [0, 255]
|
| 432 |
+
normalized_first_channel = (first_channel - first_channel.min()) / (first_channel.max() - first_channel.min()) * 255
|
| 433 |
+
# Change the data type to np.uint8
|
| 434 |
+
grayscale_image = normalized_first_channel.astype(np.uint8)
|
| 435 |
+
# Replicate the grayscale channel three times to form a BGR image
|
| 436 |
+
mask_image = np.stack((grayscale_image, grayscale_image, grayscale_image), axis=2)
|
| 437 |
+
cv2.imwrite(flow_mask_img_file, mask_image)
|
| 438 |
+
print(f"Saved mask flow visualization: {flow_mask_img_file}")
|
| 439 |
+
|
| 440 |
+
def reliable_flow_to_image(reliable_flow):
|
| 441 |
+
# Extract the first channel of the reliable_flow array
|
| 442 |
+
first_channel = reliable_flow[..., 0]
|
| 443 |
+
# Normalize the first channel to the range [0, 255]
|
| 444 |
+
normalized_first_channel = (first_channel - first_channel.min()) / (first_channel.max() - first_channel.min()) * 255
|
| 445 |
+
# Change the data type to np.uint8
|
| 446 |
+
grayscale_image = normalized_first_channel.astype(np.uint8)
|
| 447 |
+
# Replicate the grayscale channel three times to form a BGR image
|
| 448 |
+
bgr_image = np.stack((grayscale_image, grayscale_image, grayscale_image), axis=2)
|
| 449 |
+
return bgr_image
|
| 450 |
+
|
| 451 |
+
def draw_flow_lines_in_grid_in_color(img, flow, step=8, magnitude_multiplier=1, min_magnitude = 0, max_magnitude = 10000):
|
| 452 |
+
flow = flow * magnitude_multiplier
|
| 453 |
+
h, w = img.shape[:2]
|
| 454 |
+
y, x = np.mgrid[step/2:h:step, step/2:w:step].reshape(2,-1).astype(int)
|
| 455 |
+
fx, fy = flow[y,x].T
|
| 456 |
+
lines = np.vstack([x, y, x+fx, y+fy]).T.reshape(-1, 2, 2)
|
| 457 |
+
lines = np.int32(lines + 0.5)
|
| 458 |
+
vis = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 459 |
+
vis = cv2.cvtColor(vis, cv2.COLOR_GRAY2BGR)
|
| 460 |
+
|
| 461 |
+
mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
|
| 462 |
+
hsv = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8)
|
| 463 |
+
hsv[...,0] = ang*180/np.pi/2
|
| 464 |
+
hsv[...,1] = 255
|
| 465 |
+
hsv[...,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
|
| 466 |
+
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
| 467 |
+
vis = cv2.add(vis, bgr)
|
| 468 |
+
|
| 469 |
+
# Iterate through the lines
|
| 470 |
+
for (x1, y1), (x2, y2) in lines:
|
| 471 |
+
# Calculate the magnitude of the line
|
| 472 |
+
magnitude = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
|
| 473 |
+
|
| 474 |
+
# Only draw the line if it falls within the magnitude range
|
| 475 |
+
if min_magnitude <= magnitude <= max_magnitude:
|
| 476 |
+
b = int(bgr[y1, x1, 0])
|
| 477 |
+
g = int(bgr[y1, x1, 1])
|
| 478 |
+
r = int(bgr[y1, x1, 2])
|
| 479 |
+
color = (b, g, r)
|
| 480 |
+
cv2.arrowedLine(vis, (x1, y1), (x2, y2), color, thickness=1, tipLength=0.1)
|
| 481 |
+
return vis
|
| 482 |
+
|
| 483 |
+
def draw_flow_lines_in_color(img, flow, threshold=3, magnitude_multiplier=1, min_magnitude = 0, max_magnitude = 10000):
|
| 484 |
+
# h, w = img.shape[:2]
|
| 485 |
+
vis = img.copy() # Create a copy of the input image
|
| 486 |
+
|
| 487 |
+
# Find the locations in the flow field where the magnitude of the flow is greater than the threshold
|
| 488 |
+
mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
|
| 489 |
+
idx = np.where(mag > threshold)
|
| 490 |
+
|
| 491 |
+
# Create HSV image
|
| 492 |
+
hsv = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8)
|
| 493 |
+
hsv[...,0] = ang*180/np.pi/2
|
| 494 |
+
hsv[...,1] = 255
|
| 495 |
+
hsv[...,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
|
| 496 |
+
|
| 497 |
+
# Convert HSV image to BGR
|
| 498 |
+
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
| 499 |
+
|
| 500 |
+
# Add color from bgr
|
| 501 |
+
vis = cv2.add(vis, bgr)
|
| 502 |
+
|
| 503 |
+
# Draw an arrow at each of these locations to indicate the direction of the flow
|
| 504 |
+
for i, (y, x) in enumerate(zip(idx[0], idx[1])):
|
| 505 |
+
# Calculate the magnitude of the line
|
| 506 |
+
x2 = x + magnitude_multiplier * int(flow[y, x, 0])
|
| 507 |
+
y2 = y + magnitude_multiplier * int(flow[y, x, 1])
|
| 508 |
+
magnitude = np.sqrt((x2 - x)**2 + (y2 - y)**2)
|
| 509 |
+
|
| 510 |
+
# Only draw the line if it falls within the magnitude range
|
| 511 |
+
if min_magnitude <= magnitude <= max_magnitude:
|
| 512 |
+
if i % random.randint(100, 200) == 0:
|
| 513 |
+
b = int(bgr[y, x, 0])
|
| 514 |
+
g = int(bgr[y, x, 1])
|
| 515 |
+
r = int(bgr[y, x, 2])
|
| 516 |
+
color = (b, g, r)
|
| 517 |
+
cv2.arrowedLine(vis, (x, y), (x2, y2), color, thickness=1, tipLength=0.25)
|
| 518 |
+
|
| 519 |
+
return vis
|
| 520 |
+
|
| 521 |
+
def autocontrast_grayscale(image, low_cutoff=0, high_cutoff=100):
|
| 522 |
+
# Perform autocontrast on a grayscale np array image.
|
| 523 |
+
# Find the minimum and maximum values in the image
|
| 524 |
+
min_val = np.percentile(image, low_cutoff)
|
| 525 |
+
max_val = np.percentile(image, high_cutoff)
|
| 526 |
+
|
| 527 |
+
# Scale the image so that the minimum value is 0 and the maximum value is 255
|
| 528 |
+
image = 255 * (image - min_val) / (max_val - min_val)
|
| 529 |
+
|
| 530 |
+
# Clip values that fall outside the range [0, 255]
|
| 531 |
+
image = np.clip(image, 0, 255)
|
| 532 |
+
|
| 533 |
+
return image
|
| 534 |
+
|
| 535 |
+
def get_resized_image_from_filename(im, dimensions):
|
| 536 |
+
img = cv2.imread(im)
|
| 537 |
+
return cv2.resize(img, (dimensions[0], dimensions[1]), cv2.INTER_AREA)
|
| 538 |
+
|
| 539 |
+
def remap(img, flow):
|
| 540 |
+
border_mode = cv2.BORDER_REFLECT_101
|
| 541 |
+
h, w = img.shape[:2]
|
| 542 |
+
displacement = int(h * 0.25), int(w * 0.25)
|
| 543 |
+
larger_img = cv2.copyMakeBorder(img, displacement[0], displacement[0], displacement[1], displacement[1], border_mode)
|
| 544 |
+
lh, lw = larger_img.shape[:2]
|
| 545 |
+
larger_flow = extend_flow(flow, lw, lh)
|
| 546 |
+
remapped_img = cv2.remap(larger_img, larger_flow, None, cv2.INTER_LINEAR, border_mode)
|
| 547 |
+
output_img = center_crop_image(remapped_img, w, h)
|
| 548 |
+
return output_img
|
| 549 |
+
|
| 550 |
+
def center_crop_image(img, w, h):
|
| 551 |
+
y, x, _ = img.shape
|
| 552 |
+
width_indent = int((x - w) / 2)
|
| 553 |
+
height_indent = int((y - h) / 2)
|
| 554 |
+
cropped_img = img[height_indent:y-height_indent, width_indent:x-width_indent]
|
| 555 |
+
return cropped_img
|
| 556 |
+
|
| 557 |
+
def extend_flow(flow, w, h):
|
| 558 |
+
# Get the shape of the original flow image
|
| 559 |
+
flow_h, flow_w = flow.shape[:2]
|
| 560 |
+
# Calculate the position of the image in the new image
|
| 561 |
+
x_offset = int((w - flow_w) / 2)
|
| 562 |
+
y_offset = int((h - flow_h) / 2)
|
| 563 |
+
# Generate the X and Y grids
|
| 564 |
+
x_grid, y_grid = np.meshgrid(np.arange(w), np.arange(h))
|
| 565 |
+
# Create the new flow image and set it to the X and Y grids
|
| 566 |
+
new_flow = np.dstack((x_grid, y_grid)).astype(np.float32)
|
| 567 |
+
# Shift the values of the original flow by the size of the border
|
| 568 |
+
flow[:,:,0] += x_offset
|
| 569 |
+
flow[:,:,1] += y_offset
|
| 570 |
+
# Overwrite the middle of the grid with the original flow
|
| 571 |
+
new_flow[y_offset:y_offset+flow_h, x_offset:x_offset+flow_w, :] = flow
|
| 572 |
+
# Return the extended image
|
| 573 |
+
return new_flow
|
| 574 |
+
|
| 575 |
+
def abs_flow_to_rel_flow(flow, width, height):
|
| 576 |
+
fx, fy = flow[:,:,0], flow[:,:,1]
|
| 577 |
+
max_flow_x = np.max(np.abs(fx))
|
| 578 |
+
max_flow_y = np.max(np.abs(fy))
|
| 579 |
+
max_flow = max(max_flow_x, max_flow_y)
|
| 580 |
+
|
| 581 |
+
rel_fx = fx / (max_flow * width)
|
| 582 |
+
rel_fy = fy / (max_flow * height)
|
| 583 |
+
return np.dstack((rel_fx, rel_fy))
|
| 584 |
+
|
| 585 |
+
def rel_flow_to_abs_flow(rel_flow, width, height):
|
| 586 |
+
rel_fx, rel_fy = rel_flow[:,:,0], rel_flow[:,:,1]
|
| 587 |
+
|
| 588 |
+
max_flow_x = np.max(np.abs(rel_fx * width))
|
| 589 |
+
max_flow_y = np.max(np.abs(rel_fy * height))
|
| 590 |
+
max_flow = max(max_flow_x, max_flow_y)
|
| 591 |
+
|
| 592 |
+
fx = rel_fx * (max_flow * width)
|
| 593 |
+
fy = rel_fy * (max_flow * height)
|
| 594 |
+
return np.dstack((fx, fy))
|
scripts/deforum_helpers/image_sharpening.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def unsharp_mask(img, kernel_size=(5, 5), sigma=1.0, amount=1.0, threshold=0, mask=None):
|
| 5 |
+
if amount == 0:
|
| 6 |
+
return img
|
| 7 |
+
# Return a sharpened version of the image, using an unsharp mask.
|
| 8 |
+
# If mask is not None, only areas under mask are handled
|
| 9 |
+
blurred = cv2.GaussianBlur(img, kernel_size, sigma)
|
| 10 |
+
sharpened = float(amount + 1) * img - float(amount) * blurred
|
| 11 |
+
sharpened = np.maximum(sharpened, np.zeros(sharpened.shape))
|
| 12 |
+
sharpened = np.minimum(sharpened, 255 * np.ones(sharpened.shape))
|
| 13 |
+
sharpened = sharpened.round().astype(np.uint8)
|
| 14 |
+
if threshold > 0:
|
| 15 |
+
low_contrast_mask = np.absolute(img - blurred) < threshold
|
| 16 |
+
np.copyto(sharpened, img, where=low_contrast_mask)
|
| 17 |
+
if mask is not None:
|
| 18 |
+
mask = np.array(mask)
|
| 19 |
+
masked_sharpened = cv2.bitwise_and(sharpened, sharpened, mask=mask)
|
| 20 |
+
masked_img = cv2.bitwise_and(img, img, mask=255-mask)
|
| 21 |
+
sharpened = cv2.add(masked_img, masked_sharpened)
|
| 22 |
+
return sharpened
|
scripts/deforum_helpers/load_images.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import socket
|
| 5 |
+
import torchvision.transforms.functional as TF
|
| 6 |
+
from .general_utils import clean_gradio_path_strings
|
| 7 |
+
|
| 8 |
+
def load_img(path : str, shape=None, use_alpha_as_mask=False):
|
| 9 |
+
# use_alpha_as_mask: Read the alpha channel of the image as the mask image
|
| 10 |
+
image = load_image(path)
|
| 11 |
+
image = image.convert('RGBA') if use_alpha_as_mask else image.convert('RGB')
|
| 12 |
+
image = image.resize(shape, resample=Image.LANCZOS) if shape is not None else image
|
| 13 |
+
|
| 14 |
+
mask_image = None
|
| 15 |
+
if use_alpha_as_mask:
|
| 16 |
+
# Split alpha channel into a mask_image
|
| 17 |
+
red, green, blue, alpha = Image.Image.split(image) # not interested in R G or B, just in the alpha channel
|
| 18 |
+
mask_image = alpha.convert('L')
|
| 19 |
+
image = image.convert('RGB')
|
| 20 |
+
|
| 21 |
+
# check using init image alpha as mask if mask is not blank
|
| 22 |
+
extrema = mask_image.getextrema()
|
| 23 |
+
if (extrema == (0,0)) or extrema == (255,255):
|
| 24 |
+
print("use_alpha_as_mask==True: Using the alpha channel from the init image as a mask, but the alpha channel is blank.")
|
| 25 |
+
print("ignoring alpha as mask.")
|
| 26 |
+
mask_image = None
|
| 27 |
+
|
| 28 |
+
return image, mask_image
|
| 29 |
+
|
| 30 |
+
def load_image(image_path :str):
|
| 31 |
+
image_path = clean_gradio_path_strings(image_path)
|
| 32 |
+
image = None
|
| 33 |
+
if image_path.startswith('http://') or image_path.startswith('https://'):
|
| 34 |
+
try:
|
| 35 |
+
host = socket.gethostbyname("www.google.com")
|
| 36 |
+
s = socket.create_connection((host, 80), 2)
|
| 37 |
+
s.close()
|
| 38 |
+
except:
|
| 39 |
+
raise ConnectionError("There is no active internet connection available (couldn't connect to google.com as a network test) - please use *local* masks and init files only.")
|
| 40 |
+
try:
|
| 41 |
+
response = requests.get(image_path, stream=True)
|
| 42 |
+
except requests.exceptions.RequestException as e:
|
| 43 |
+
raise ConnectionError("Failed to download image due to no internet connection. Error: {}".format(e))
|
| 44 |
+
if response.status_code == 404 or response.status_code != 200:
|
| 45 |
+
raise ConnectionError("Init image url or mask image url is not valid")
|
| 46 |
+
image = Image.open(response.raw).convert('RGB')
|
| 47 |
+
else:
|
| 48 |
+
if not os.path.exists(image_path):
|
| 49 |
+
raise RuntimeError("Init image path or mask image path is not valid")
|
| 50 |
+
image = Image.open(image_path).convert('RGB')
|
| 51 |
+
|
| 52 |
+
return image
|
| 53 |
+
|
| 54 |
+
def prepare_mask(mask_input, mask_shape, mask_brightness_adjust=1.0, mask_contrast_adjust=1.0):
|
| 55 |
+
"""
|
| 56 |
+
prepares mask for use in webui
|
| 57 |
+
"""
|
| 58 |
+
if isinstance(mask_input, Image.Image):
|
| 59 |
+
mask = mask_input
|
| 60 |
+
else :
|
| 61 |
+
mask = load_image(mask_input)
|
| 62 |
+
mask = mask.resize(mask_shape, resample=Image.LANCZOS)
|
| 63 |
+
if mask_brightness_adjust != 1:
|
| 64 |
+
mask = TF.adjust_brightness(mask, mask_brightness_adjust)
|
| 65 |
+
if mask_contrast_adjust != 1:
|
| 66 |
+
mask = TF.adjust_contrast(mask, mask_contrast_adjust)
|
| 67 |
+
mask = mask.convert('L')
|
| 68 |
+
return mask
|
| 69 |
+
|
| 70 |
+
# "check_mask_for_errors" may have prevented errors in composable masks,
|
| 71 |
+
# but it CAUSES errors on any frame where it's all black.
|
| 72 |
+
# Bypassing the check below until we can fix it even better.
|
| 73 |
+
# This may break composable masks, but it makes ACTUAL masks usable.
|
| 74 |
+
def check_mask_for_errors(mask_input, invert_mask=False):
|
| 75 |
+
extrema = mask_input.getextrema()
|
| 76 |
+
if (invert_mask):
|
| 77 |
+
if extrema == (255,255):
|
| 78 |
+
print("after inverting mask will be blank. ignoring mask")
|
| 79 |
+
return None
|
| 80 |
+
elif extrema == (0,0):
|
| 81 |
+
print("mask is blank. ignoring mask")
|
| 82 |
+
return None
|
| 83 |
+
else:
|
| 84 |
+
return mask_input
|
| 85 |
+
|
| 86 |
+
def get_mask(args):
|
| 87 |
+
return prepare_mask(args.mask_file, (args.W, args.H), args.mask_contrast_adjust, args.mask_brightness_adjust)
|
| 88 |
+
|
| 89 |
+
def get_mask_from_file(mask_file, args):
|
| 90 |
+
return prepare_mask(mask_file, (args.W, args.H), args.mask_contrast_adjust, args.mask_brightness_adjust)
|
| 91 |
+
|
| 92 |
+
def blank_if_none(mask, w, h, mode):
|
| 93 |
+
return Image.new(mode, (w, h), (0)) if mask is None else mask
|
| 94 |
+
|
| 95 |
+
def none_if_blank(mask):
|
| 96 |
+
return None if mask.getextrema() == (0,0) else mask
|
scripts/deforum_helpers/masks.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import gc
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image, ImageOps
|
| 6 |
+
from .video_audio_utilities import get_frame_name
|
| 7 |
+
from .load_images import load_image
|
| 8 |
+
|
| 9 |
+
def do_overlay_mask(args, anim_args, img, frame_idx, is_bgr_array=False):
|
| 10 |
+
if is_bgr_array:
|
| 11 |
+
img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2RGB)
|
| 12 |
+
img = Image.fromarray(img)
|
| 13 |
+
|
| 14 |
+
if anim_args.use_mask_video:
|
| 15 |
+
current_mask = Image.open(os.path.join(args.outdir, 'maskframes', get_frame_name(anim_args.video_mask_path) + f"{frame_idx:09}.jpg"))
|
| 16 |
+
current_frame = Image.open(os.path.join(args.outdir, 'inputframes', get_frame_name(anim_args.video_init_path) + f"{frame_idx:09}.jpg"))
|
| 17 |
+
elif args.use_mask:
|
| 18 |
+
current_mask = args.mask_image if args.mask_image is not None else load_image(args.mask_file)
|
| 19 |
+
if args.init_image is None:
|
| 20 |
+
current_frame = img
|
| 21 |
+
else:
|
| 22 |
+
current_frame = load_image(args.init_image)
|
| 23 |
+
|
| 24 |
+
current_mask = current_mask.resize((args.W, args.H), Image.LANCZOS)
|
| 25 |
+
current_frame = current_frame.resize((args.W, args.H), Image.LANCZOS)
|
| 26 |
+
current_mask = ImageOps.grayscale(current_mask)
|
| 27 |
+
|
| 28 |
+
if args.invert_mask:
|
| 29 |
+
current_mask = ImageOps.invert(current_mask)
|
| 30 |
+
|
| 31 |
+
img = Image.composite(img, current_frame, current_mask)
|
| 32 |
+
|
| 33 |
+
if is_bgr_array:
|
| 34 |
+
img = np.array(img)
|
| 35 |
+
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
| 36 |
+
|
| 37 |
+
del(current_mask, current_frame)
|
| 38 |
+
gc.collect()
|
| 39 |
+
|
| 40 |
+
return img
|
scripts/deforum_helpers/noise.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch.nn.functional import interpolate
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import ImageOps
|
| 5 |
+
import math
|
| 6 |
+
from .animation import sample_to_cv2
|
| 7 |
+
import cv2
|
| 8 |
+
from modules.shared import opts
|
| 9 |
+
|
| 10 |
+
DEBUG_MODE = opts.data.get("deforum_debug_mode_enabled", False)
|
| 11 |
+
|
| 12 |
+
deforum_noise_gen = torch.Generator(device='cpu')
|
| 13 |
+
|
| 14 |
+
# 2D Perlin noise in PyTorch https://gist.github.com/vadimkantorov/ac1b097753f217c5c11bc2ff396e0a57
|
| 15 |
+
def rand_perlin_2d(shape, res, fade = lambda t: 6*t**5 - 15*t**4 + 10*t**3):
|
| 16 |
+
delta = (res[0] / shape[0], res[1] / shape[1])
|
| 17 |
+
d = (shape[0] // res[0], shape[1] // res[1])
|
| 18 |
+
|
| 19 |
+
grid = torch.stack(torch.meshgrid(torch.arange(0, res[0], delta[0]), torch.arange(0, res[1], delta[1]), indexing='ij'), dim = -1) % 1
|
| 20 |
+
angles = 2*math.pi*torch.rand(res[0]+1, res[1]+1, generator=deforum_noise_gen)
|
| 21 |
+
gradients = torch.stack((torch.cos(angles), torch.sin(angles)), dim = -1)
|
| 22 |
+
|
| 23 |
+
tile_grads = lambda slice1, slice2: gradients[slice1[0]:slice1[1], slice2[0]:slice2[1]].repeat_interleave(d[0], 0).repeat_interleave(d[1], 1)
|
| 24 |
+
dot = lambda grad, shift: (torch.stack((grid[:shape[0],:shape[1],0] + shift[0], grid[:shape[0],:shape[1], 1] + shift[1] ), dim = -1) * grad[:shape[0], :shape[1]]).sum(dim = -1)
|
| 25 |
+
|
| 26 |
+
n00 = dot(tile_grads([0, -1], [0, -1]), [0, 0])
|
| 27 |
+
n10 = dot(tile_grads([1, None], [0, -1]), [-1, 0])
|
| 28 |
+
n01 = dot(tile_grads([0, -1],[1, None]), [0, -1])
|
| 29 |
+
n11 = dot(tile_grads([1, None], [1, None]), [-1,-1])
|
| 30 |
+
t = fade(grid[:shape[0], :shape[1]])
|
| 31 |
+
return math.sqrt(2) * torch.lerp(torch.lerp(n00, n10, t[..., 0]), torch.lerp(n01, n11, t[..., 0]), t[..., 1])
|
| 32 |
+
|
| 33 |
+
def rand_perlin_2d_octaves(shape, res, octaves=1, persistence=0.5):
|
| 34 |
+
noise = torch.zeros(shape)
|
| 35 |
+
frequency = 1
|
| 36 |
+
amplitude = 1
|
| 37 |
+
for _ in range(int(octaves)):
|
| 38 |
+
noise += amplitude * rand_perlin_2d(shape, (frequency*res[0], frequency*res[1]))
|
| 39 |
+
frequency *= 2
|
| 40 |
+
amplitude *= persistence
|
| 41 |
+
return noise
|
| 42 |
+
|
| 43 |
+
def condition_noise_mask(noise_mask, invert_mask = False):
|
| 44 |
+
if invert_mask:
|
| 45 |
+
noise_mask = ImageOps.invert(noise_mask)
|
| 46 |
+
noise_mask = np.array(noise_mask.convert("L"))
|
| 47 |
+
noise_mask = noise_mask.astype(np.float32) / 255.0
|
| 48 |
+
noise_mask = np.around(noise_mask, decimals=0)
|
| 49 |
+
noise_mask = torch.from_numpy(noise_mask)
|
| 50 |
+
#noise_mask = torch.round(noise_mask)
|
| 51 |
+
return noise_mask
|
| 52 |
+
|
| 53 |
+
def add_noise(sample, noise_amt: float, seed: int, noise_type: str, noise_args, noise_mask = None, invert_mask = False):
|
| 54 |
+
deforum_noise_gen.manual_seed(seed) # Reproducibility
|
| 55 |
+
perlin_w = sample.shape[0]
|
| 56 |
+
perlin_h = sample.shape[1]
|
| 57 |
+
perlin_w, perlin_h = map(lambda x: x - x % 64, (perlin_w, perlin_h)) # rescale perlin to multiplies of 64
|
| 58 |
+
sample2dshape = (perlin_w, perlin_h)
|
| 59 |
+
noise = torch.randn((sample.shape[2], perlin_w, perlin_h), generator=deforum_noise_gen) # White noise
|
| 60 |
+
if noise_type == 'perlin':
|
| 61 |
+
# rand_perlin_2d_octaves is between -1 and 1, so we need to shift it to be between 0 and 1
|
| 62 |
+
# print(sample.shape)
|
| 63 |
+
noise = noise * ((rand_perlin_2d_octaves(sample2dshape, (int(noise_args[0]), int(noise_args[1])), octaves=noise_args[2], persistence=noise_args[3]) + torch.ones(sample2dshape)) / 2)
|
| 64 |
+
noise = interpolate(noise.unsqueeze(1), size=(sample.shape[0], sample.shape[1])).squeeze(1) # rescale perlin back to the target resolution
|
| 65 |
+
if noise_mask is not None:
|
| 66 |
+
noise_mask = condition_noise_mask(noise_mask, invert_mask)
|
| 67 |
+
noise_to_add = sample_to_cv2(noise * noise_mask)
|
| 68 |
+
else:
|
| 69 |
+
noise_to_add = sample_to_cv2(noise)
|
| 70 |
+
sample = cv2.addWeighted(sample, 1-noise_amt, noise_to_add, noise_amt, 0)
|
| 71 |
+
|
| 72 |
+
return sample
|
scripts/deforum_helpers/parseq_adapter.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import operator
|
| 5 |
+
from operator import itemgetter
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import requests
|
| 9 |
+
from .animation_key_frames import DeformAnimKeys, ControlNetKeys
|
| 10 |
+
from .rich import console
|
| 11 |
+
|
| 12 |
+
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
|
| 13 |
+
|
| 14 |
+
class ParseqAdapter():
|
| 15 |
+
def __init__(self, parseq_args, anim_args, video_args, controlnet_args, mute=False):
|
| 16 |
+
|
| 17 |
+
# Basic data extraction
|
| 18 |
+
self.use_parseq = parseq_args.parseq_manifest and parseq_args.parseq_manifest.strip()
|
| 19 |
+
self.use_deltas = parseq_args.parseq_use_deltas
|
| 20 |
+
|
| 21 |
+
self.parseq_json = self.load_manifest(parseq_args) if self.use_parseq else json.loads('{ "rendered_frames": [{"frame": 0}] }')
|
| 22 |
+
self.rendered_frames = self.parseq_json['rendered_frames']
|
| 23 |
+
self.max_frame = self.get_max('frame')
|
| 24 |
+
self.required_frames = anim_args.max_frames
|
| 25 |
+
|
| 26 |
+
# Wrap the original schedules with Parseq decorators, so that Parseq values will override the original values IFF appropriate.
|
| 27 |
+
self.anim_keys = ParseqAnimKeysDecorator(self, DeformAnimKeys(anim_args))
|
| 28 |
+
self.cn_keys = ParseqControlNetKeysDecorator(self, ControlNetKeys(anim_args, controlnet_args)) if controlnet_args else None
|
| 29 |
+
|
| 30 |
+
# Validation
|
| 31 |
+
if (self.use_parseq):
|
| 32 |
+
self.required_fps = video_args.fps
|
| 33 |
+
self.config_output_fps = self.parseq_json['options']['output_fps']
|
| 34 |
+
count_defined_frames = len(self.rendered_frames)
|
| 35 |
+
expected_defined_frames = self.max_frame+1 # frames are 0-indexed
|
| 36 |
+
if (expected_defined_frames != count_defined_frames):
|
| 37 |
+
logging.warning(f"There may be duplicated or missing frame data in the Parseq input: expected {expected_defined_frames} frames including frame 0 because the highest frame number is {self.max_frame}, but there are {count_defined_frames} frames defined.")
|
| 38 |
+
if not mute:
|
| 39 |
+
self.print_parseq_table()
|
| 40 |
+
|
| 41 |
+
# Resolve manifest either directly from supplied value or via supplied URL
|
| 42 |
+
def load_manifest(self, parseq_args):
|
| 43 |
+
manifestOrUrl = parseq_args.parseq_manifest.strip()
|
| 44 |
+
if (manifestOrUrl.startswith('http')):
|
| 45 |
+
logging.info(f"Loading Parseq manifest from URL: {manifestOrUrl}")
|
| 46 |
+
try:
|
| 47 |
+
body = requests.get(manifestOrUrl).text
|
| 48 |
+
logging.debug(f"Loaded remote manifest: {body}")
|
| 49 |
+
parseq_json = json.loads(body)
|
| 50 |
+
if not parseq_json or not 'rendered_frames' in parseq_json:
|
| 51 |
+
raise Exception(f"The JSON data does not look like a Parseq manifest (missing field 'rendered_frames').")
|
| 52 |
+
|
| 53 |
+
# SIDE EFFECT!
|
| 54 |
+
# Add the parseq manifest without the detailed frame data to parseq_args.
|
| 55 |
+
# This ensures it will be saved in the settings file, so that you can always
|
| 56 |
+
# see exactly what parseq prompts and keyframes were used, even if what the URL
|
| 57 |
+
# points to changes.
|
| 58 |
+
parseq_args.fetched_parseq_manifest_summary = copy.deepcopy(parseq_json)
|
| 59 |
+
if parseq_args.fetched_parseq_manifest_summary['rendered_frames']:
|
| 60 |
+
del parseq_args.fetched_parseq_manifest_summary['rendered_frames']
|
| 61 |
+
if parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']:
|
| 62 |
+
del parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']
|
| 63 |
+
|
| 64 |
+
return parseq_json
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logging.error(f"Unable to load Parseq manifest from URL: {manifestOrUrl}")
|
| 68 |
+
raise e
|
| 69 |
+
else:
|
| 70 |
+
return json.loads(manifestOrUrl)
|
| 71 |
+
|
| 72 |
+
def print_parseq_table(self):
|
| 73 |
+
from rich.table import Table
|
| 74 |
+
from rich import box
|
| 75 |
+
|
| 76 |
+
table = Table(padding=0, box=box.ROUNDED, show_lines=True)
|
| 77 |
+
table.add_column("", style="white bold")
|
| 78 |
+
table.add_column("Parseq", style="cyan")
|
| 79 |
+
table.add_column("Deforum", style="green")
|
| 80 |
+
|
| 81 |
+
table.add_row("Anim Fields", '\n'.join(self.anim_keys.managed_fields()), '\n'.join(self.anim_keys.unmanaged_fields()))
|
| 82 |
+
if self.cn_keys:
|
| 83 |
+
table.add_row("Cn Fields", '\n'.join(self.cn_keys.managed_fields()), '\n'.join(self.cn_keys.unmanaged_fields()))
|
| 84 |
+
table.add_row("Prompts", "✅" if self.manages_prompts() else "❌", "✅" if not self.manages_prompts() else "❌")
|
| 85 |
+
table.add_row("Frames", str(len(self.rendered_frames)), str(self.required_frames) + (" ⚠️" if str(self.required_frames) != str(len(self.rendered_frames))+"" else ""))
|
| 86 |
+
table.add_row("FPS", str(self.config_output_fps), str(self.required_fps) + (" ⚠️" if str(self.required_fps) != str(self.config_output_fps) else ""))
|
| 87 |
+
|
| 88 |
+
console.print("\nUse this table to validate your Parseq & Deforum setup:")
|
| 89 |
+
console.print(table)
|
| 90 |
+
|
| 91 |
+
def manages_prompts(self):
|
| 92 |
+
return self.use_parseq and 'deforum_prompt' in self.rendered_frames[0].keys()
|
| 93 |
+
|
| 94 |
+
def manages_seed(self):
|
| 95 |
+
return self.use_parseq and 'seed' in self.rendered_frames[0].keys()
|
| 96 |
+
|
| 97 |
+
def get_max(self, seriesName):
|
| 98 |
+
return max(self.rendered_frames, key=itemgetter(seriesName))[seriesName]
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class ParseqAbstractDecorator():
|
| 102 |
+
|
| 103 |
+
def __init__(self, adapter: ParseqAdapter, fallback_keys):
|
| 104 |
+
self.adapter = adapter
|
| 105 |
+
self.fallback_keys = fallback_keys
|
| 106 |
+
|
| 107 |
+
def parseq_to_series(self, seriesName):
|
| 108 |
+
|
| 109 |
+
# Check if value is present in first frame of JSON data. If not, assume it's undefined.
|
| 110 |
+
# The Parseq contract is that the first frame (at least) must define values for all fields.
|
| 111 |
+
try:
|
| 112 |
+
if self.adapter.rendered_frames[0][seriesName] is not None:
|
| 113 |
+
logging.debug(f"Found {seriesName} in first frame of Parseq data. Assuming it's defined.")
|
| 114 |
+
except KeyError:
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
key_frame_series = pd.Series([np.nan for a in range(self.adapter.required_frames)])
|
| 118 |
+
|
| 119 |
+
for frame in self.adapter.rendered_frames:
|
| 120 |
+
frame_idx = frame['frame']
|
| 121 |
+
if frame_idx < self.adapter.required_frames:
|
| 122 |
+
if not np.isnan(key_frame_series[frame_idx]):
|
| 123 |
+
logging.warning(f"Duplicate frame definition {frame_idx} detected for data {seriesName}. Latest wins.")
|
| 124 |
+
key_frame_series[frame_idx] = frame[seriesName]
|
| 125 |
+
|
| 126 |
+
# If the animation will have more frames than Parseq defines,
|
| 127 |
+
# duplicate final value to match the required frame count.
|
| 128 |
+
while (frame_idx < self.adapter.required_frames):
|
| 129 |
+
key_frame_series[frame_idx] = operator.itemgetter(-1)(self.adapter.rendered_frames)[seriesName]
|
| 130 |
+
frame_idx += 1
|
| 131 |
+
|
| 132 |
+
return key_frame_series
|
| 133 |
+
|
| 134 |
+
# fallback to anim_args if the series is not defined in the Parseq data
|
| 135 |
+
def __getattribute__(inst, name):
|
| 136 |
+
try:
|
| 137 |
+
definedField = super(ParseqAbstractDecorator, inst).__getattribute__(name)
|
| 138 |
+
except AttributeError:
|
| 139 |
+
# No field with this name has been explicitly extracted from the JSON data.
|
| 140 |
+
# It must be a new parameter. Let's see if it's in the raw JSON.
|
| 141 |
+
|
| 142 |
+
parseqName = inst.strip_suffixes(name)
|
| 143 |
+
|
| 144 |
+
# returns None if not defined in Parseq JSON data
|
| 145 |
+
definedField = inst.parseq_to_series(parseqName)
|
| 146 |
+
if (definedField is not None):
|
| 147 |
+
# add the field to the instance so we don't compute it again.
|
| 148 |
+
setattr(inst, name, definedField)
|
| 149 |
+
|
| 150 |
+
if (definedField is not None):
|
| 151 |
+
return definedField
|
| 152 |
+
else:
|
| 153 |
+
logging.debug(f"Data for {name} not defined in Parseq data. Falling back to standard Deforum values.")
|
| 154 |
+
return getattr(inst.fallback_keys, name)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# parseq doesn't use _series, _schedule or _schedule_series suffixes in the
|
| 158 |
+
# JSON data - remove them.
|
| 159 |
+
def strip_suffixes(self, name):
|
| 160 |
+
strippableSuffixes = ['_series', '_schedule']
|
| 161 |
+
parseqName = name
|
| 162 |
+
while any(parseqName.endswith(suffix) for suffix in strippableSuffixes):
|
| 163 |
+
for suffix in strippableSuffixes:
|
| 164 |
+
if parseqName.endswith(suffix):
|
| 165 |
+
parseqName = parseqName[:-len(suffix)]
|
| 166 |
+
return parseqName
|
| 167 |
+
|
| 168 |
+
# parseq prefixes some field names for clarity. These prefixes are not present in the original Deforum names.
|
| 169 |
+
def strip_parseq_prefixes(self, name):
|
| 170 |
+
strippablePrefixes = ['guided_']
|
| 171 |
+
parseqName = name
|
| 172 |
+
while any(parseqName.startswith(prefix) for prefix in strippablePrefixes):
|
| 173 |
+
for prefix in strippablePrefixes:
|
| 174 |
+
if parseqName.startswith(prefix):
|
| 175 |
+
parseqName = parseqName[len(prefix):]
|
| 176 |
+
return parseqName
|
| 177 |
+
|
| 178 |
+
def all_parseq_fields(self):
|
| 179 |
+
return [self.strip_parseq_prefixes(field) for field in self.adapter.rendered_frames[0].keys() if (not field.endswith('_delta') and not field.endswith('_pc'))]
|
| 180 |
+
|
| 181 |
+
def managed_fields(self):
|
| 182 |
+
all_parseq_fields = self.all_parseq_fields()
|
| 183 |
+
deforum_fields = [self.strip_suffixes(property) for property, _ in vars(self.fallback_keys).items() if property not in ['fi'] and not property.startswith('_')]
|
| 184 |
+
return [field for field in deforum_fields if field in all_parseq_fields]
|
| 185 |
+
|
| 186 |
+
def unmanaged_fields(self):
|
| 187 |
+
all_parseq_fields = self.all_parseq_fields()
|
| 188 |
+
deforum_fields = [self.strip_suffixes(property) for property, _ in vars(self.fallback_keys).items() if property not in ['fi'] and not property.startswith('_')]
|
| 189 |
+
return [field for field in deforum_fields if field not in all_parseq_fields]
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class ParseqControlNetKeysDecorator(ParseqAbstractDecorator):
|
| 193 |
+
def __init__(self, adapter: ParseqAdapter, cn_keys):
|
| 194 |
+
super().__init__(adapter, cn_keys)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class ParseqAnimKeysDecorator(ParseqAbstractDecorator):
|
| 198 |
+
def __init__(self, adapter: ParseqAdapter, anim_keys):
|
| 199 |
+
super().__init__(adapter, anim_keys)
|
| 200 |
+
|
| 201 |
+
# Parseq treats input values as absolute values. So if you want to
|
| 202 |
+
# progressively rotate 180 degrees over 4 frames, you specify: 45, 90, 135, 180.
|
| 203 |
+
# However, many animation parameters are relative to the previous frame if there is enough
|
| 204 |
+
# loopback strength. So if you want to rotate 180 degrees over 5 frames, the animation engine expects:
|
| 205 |
+
# 45, 45, 45, 45. Therefore, for such parameter, we use the fact that Parseq supplies delta values.
|
| 206 |
+
optional_delta = '_delta' if self.adapter.use_deltas else ''
|
| 207 |
+
self.angle_series = super().parseq_to_series('angle' + optional_delta)
|
| 208 |
+
self.zoom_series = super().parseq_to_series('zoom' + optional_delta)
|
| 209 |
+
self.translation_x_series = super().parseq_to_series('translation_x' + optional_delta)
|
| 210 |
+
self.translation_y_series = super().parseq_to_series('translation_y' + optional_delta)
|
| 211 |
+
self.translation_z_series = super().parseq_to_series('translation_z' + optional_delta)
|
| 212 |
+
self.rotation_3d_x_series = super().parseq_to_series('rotation_3d_x' + optional_delta)
|
| 213 |
+
self.rotation_3d_y_series = super().parseq_to_series('rotation_3d_y' + optional_delta)
|
| 214 |
+
self.rotation_3d_z_series = super().parseq_to_series('rotation_3d_z' + optional_delta)
|
| 215 |
+
self.perspective_flip_theta_series = super().parseq_to_series('perspective_flip_theta' + optional_delta)
|
| 216 |
+
self.perspective_flip_phi_series = super().parseq_to_series('perspective_flip_phi' + optional_delta)
|
| 217 |
+
self.perspective_flip_gamma_series = super().parseq_to_series('perspective_flip_gamma' + optional_delta)
|
| 218 |
+
|
| 219 |
+
# Non-motion animation args - never use deltas for these.
|
| 220 |
+
self.perspective_flip_fv_series = super().parseq_to_series('perspective_flip_fv')
|
| 221 |
+
self.noise_schedule_series = super().parseq_to_series('noise')
|
| 222 |
+
self.strength_schedule_series = super().parseq_to_series('strength')
|
| 223 |
+
self.sampler_schedule_series = super().parseq_to_series('sampler_schedule')
|
| 224 |
+
self.contrast_schedule_series = super().parseq_to_series('contrast')
|
| 225 |
+
self.cfg_scale_schedule_series = super().parseq_to_series('scale')
|
| 226 |
+
self.steps_schedule_series = super().parseq_to_series("steps_schedule")
|
| 227 |
+
self.seed_schedule_series = super().parseq_to_series('seed')
|
| 228 |
+
self.fov_series = super().parseq_to_series('fov')
|
| 229 |
+
self.near_series = super().parseq_to_series('near')
|
| 230 |
+
self.far_series = super().parseq_to_series('far')
|
| 231 |
+
self.subseed_schedule_series = super().parseq_to_series('subseed')
|
| 232 |
+
self.subseed_strength_schedule_series = super().parseq_to_series('subseed_strength')
|
| 233 |
+
self.kernel_schedule_series = super().parseq_to_series('antiblur_kernel')
|
| 234 |
+
self.sigma_schedule_series = super().parseq_to_series('antiblur_sigma')
|
| 235 |
+
self.amount_schedule_series = super().parseq_to_series('antiblur_amount')
|
| 236 |
+
self.threshold_schedule_series = super().parseq_to_series('antiblur_threshold')
|
| 237 |
+
|
| 238 |
+
# TODO - move to a different decorator?
|
| 239 |
+
self.prompts = super().parseq_to_series('deforum_prompt') # formatted as "{positive} --neg {negative}"
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
|
scripts/deforum_helpers/parseq_adapter_test.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
##
|
| 2 |
+
# From /scripts directory, run like: python -m unittest deforum_helpers.parseq_adapter_test
|
| 3 |
+
##
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
from .parseq_adapter import ParseqAdapter
|
| 7 |
+
from .animation_key_frames import DeformAnimKeys, LooperAnimKeys, ControlNetKeys
|
| 8 |
+
from unittest.mock import patch
|
| 9 |
+
from unittest.mock import MagicMock, PropertyMock
|
| 10 |
+
from types import SimpleNamespace
|
| 11 |
+
|
| 12 |
+
DEFAULT_ARGS = SimpleNamespace(anim_args = SimpleNamespace(max_frames=2),
|
| 13 |
+
video_args = SimpleNamespace(fps=30),
|
| 14 |
+
args = SimpleNamespace(seed=-1),
|
| 15 |
+
loop_args = SimpleNamespace(),
|
| 16 |
+
controlnet_args = SimpleNamespace())
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def buildParseqAdapter(parseq_use_deltas, parseq_manifest, setup_args=DEFAULT_ARGS):
|
| 20 |
+
return ParseqAdapter(SimpleNamespace(parseq_use_deltas=parseq_use_deltas, parseq_manifest=parseq_manifest),
|
| 21 |
+
setup_args.anim_args, setup_args.video_args, setup_args.controlnet_args)
|
| 22 |
+
|
| 23 |
+
class TestParseqAnimKeys(unittest.TestCase):
|
| 24 |
+
|
| 25 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 26 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 27 |
+
def test_withprompt(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 28 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=True, parseq_manifest="""
|
| 29 |
+
{
|
| 30 |
+
"options": {
|
| 31 |
+
"output_fps": 30
|
| 32 |
+
},
|
| 33 |
+
"rendered_frames": [
|
| 34 |
+
{
|
| 35 |
+
"frame": 0,
|
| 36 |
+
"deforum_prompt": "blah"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"frame": 1,
|
| 40 |
+
"deforum_prompt": "blah"
|
| 41 |
+
}
|
| 42 |
+
]
|
| 43 |
+
}
|
| 44 |
+
""")
|
| 45 |
+
self.assertTrue(parseq_adapter.manages_prompts())
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 49 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 50 |
+
def test_withoutprompt(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 51 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=True, parseq_manifest="""
|
| 52 |
+
{
|
| 53 |
+
"options": {
|
| 54 |
+
"output_fps": 30
|
| 55 |
+
},
|
| 56 |
+
"rendered_frames": [
|
| 57 |
+
{
|
| 58 |
+
"frame": 0
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"frame": 1
|
| 62 |
+
}
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
""")
|
| 66 |
+
self.assertFalse(parseq_adapter.manages_prompts())
|
| 67 |
+
|
| 68 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 69 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 70 |
+
def test_withseed(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 71 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=True, parseq_manifest="""
|
| 72 |
+
{
|
| 73 |
+
"options": {
|
| 74 |
+
"output_fps": 30
|
| 75 |
+
},
|
| 76 |
+
"rendered_frames": [
|
| 77 |
+
{
|
| 78 |
+
"frame": 0,
|
| 79 |
+
"seed": 1
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"frame": 1,
|
| 83 |
+
"seed": 2
|
| 84 |
+
}
|
| 85 |
+
]
|
| 86 |
+
}
|
| 87 |
+
""")
|
| 88 |
+
self.assertTrue(parseq_adapter.manages_seed())
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 92 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 93 |
+
def test_withoutseed(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 94 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=True, parseq_manifest="""
|
| 95 |
+
{
|
| 96 |
+
"options": {
|
| 97 |
+
"output_fps": 30
|
| 98 |
+
},
|
| 99 |
+
"rendered_frames": [
|
| 100 |
+
{
|
| 101 |
+
"frame": 0
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"frame": 1
|
| 105 |
+
}
|
| 106 |
+
]
|
| 107 |
+
}
|
| 108 |
+
""")
|
| 109 |
+
self.assertFalse(parseq_adapter.manages_seed())
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 113 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 114 |
+
def test_usedelta(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 115 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=True, parseq_manifest="""
|
| 116 |
+
{
|
| 117 |
+
"options": {
|
| 118 |
+
"output_fps": 30
|
| 119 |
+
},
|
| 120 |
+
"rendered_frames": [
|
| 121 |
+
{
|
| 122 |
+
"frame": 0,
|
| 123 |
+
"angle": 90,
|
| 124 |
+
"angle_delta": 90
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"frame": 1,
|
| 128 |
+
"angle": 180,
|
| 129 |
+
"angle_delta": 90
|
| 130 |
+
}
|
| 131 |
+
]
|
| 132 |
+
}
|
| 133 |
+
""")
|
| 134 |
+
self.assertEqual(parseq_adapter.anim_keys.angle_series[1], 90)
|
| 135 |
+
|
| 136 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 137 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 138 |
+
def test_usenondelta(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 139 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=False, parseq_manifest="""
|
| 140 |
+
{
|
| 141 |
+
"options": {
|
| 142 |
+
"output_fps": 30
|
| 143 |
+
},
|
| 144 |
+
"rendered_frames": [
|
| 145 |
+
{
|
| 146 |
+
"frame": 0,
|
| 147 |
+
"angle": 90,
|
| 148 |
+
"angle_delta": 90
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"frame": 1,
|
| 152 |
+
"angle": 180,
|
| 153 |
+
"angle_delta": 90
|
| 154 |
+
}
|
| 155 |
+
]
|
| 156 |
+
}
|
| 157 |
+
""")
|
| 158 |
+
self.assertEqual(parseq_adapter.anim_keys.angle_series[1], 180)
|
| 159 |
+
|
| 160 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 161 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 162 |
+
def test_fallbackonundefined(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 163 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=False, parseq_manifest="""
|
| 164 |
+
{
|
| 165 |
+
"options": {
|
| 166 |
+
"output_fps": 30
|
| 167 |
+
},
|
| 168 |
+
"rendered_frames": [
|
| 169 |
+
{
|
| 170 |
+
"frame": 0
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"frame": 1
|
| 174 |
+
}
|
| 175 |
+
]
|
| 176 |
+
}
|
| 177 |
+
""")
|
| 178 |
+
#TODO - this is a hacky check to make sure we're falling back to the mock.
|
| 179 |
+
#There must be a better way to inject an expected value via patch and check for that...
|
| 180 |
+
self.assertRegex(str(parseq_adapter.anim_keys.angle_series[0]), r'MagicMock')
|
| 181 |
+
|
| 182 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 183 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 184 |
+
def test_cn(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 185 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=False, parseq_manifest="""
|
| 186 |
+
{
|
| 187 |
+
"options": {
|
| 188 |
+
"output_fps": 30
|
| 189 |
+
},
|
| 190 |
+
"rendered_frames": [
|
| 191 |
+
{
|
| 192 |
+
"frame": 0,
|
| 193 |
+
"cn_1_weight": 1
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"frame": 1,
|
| 197 |
+
"cn_1_weight": 1
|
| 198 |
+
}
|
| 199 |
+
]
|
| 200 |
+
}
|
| 201 |
+
""")
|
| 202 |
+
self.assertEqual(parseq_adapter.cn_keys.cn_1_weight_schedule_series[0], 1)
|
| 203 |
+
|
| 204 |
+
@patch('deforum_helpers.parseq_adapter.DeformAnimKeys')
|
| 205 |
+
@patch('deforum_helpers.parseq_adapter.ControlNetKeys')
|
| 206 |
+
def test_cn_fallback(self, mock_deformanimkeys, mock_controlnetkeys):
|
| 207 |
+
parseq_adapter = buildParseqAdapter(parseq_use_deltas=False, parseq_manifest="""
|
| 208 |
+
{
|
| 209 |
+
"options": {
|
| 210 |
+
"output_fps": 30
|
| 211 |
+
},
|
| 212 |
+
"rendered_frames": [
|
| 213 |
+
{
|
| 214 |
+
"frame": 0
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"frame": 1
|
| 218 |
+
}
|
| 219 |
+
]
|
| 220 |
+
}
|
| 221 |
+
""")
|
| 222 |
+
#TODO - this is a hacky check to make sure we're falling back to the mock.
|
| 223 |
+
#There must be a better way to inject an expected value via patch and check for that...
|
| 224 |
+
self.assertRegex(str(parseq_adapter.cn_keys.cn_1_weight_schedule_series[0]), r'MagicMock')
|
| 225 |
+
|
| 226 |
+
if __name__ == '__main__':
|
| 227 |
+
unittest.main()
|
scripts/deforum_helpers/prompt.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import numexpr
|
| 3 |
+
|
| 4 |
+
def check_is_number(value):
|
| 5 |
+
float_pattern = r'^(?=.)([+-]?([0-9]*)(\.([0-9]+))?)$'
|
| 6 |
+
return re.match(float_pattern, value)
|
| 7 |
+
|
| 8 |
+
def parse_weight(match, frame=0, max_frames=0) -> float:
|
| 9 |
+
w_raw = match.group("weight")
|
| 10 |
+
max_f = max_frames # this line has to be left intact as it's in use by numexpr even though it looks like it doesn't
|
| 11 |
+
if w_raw is None:
|
| 12 |
+
return 1
|
| 13 |
+
if check_is_number(w_raw):
|
| 14 |
+
return float(w_raw)
|
| 15 |
+
else:
|
| 16 |
+
t = frame
|
| 17 |
+
if len(w_raw) < 3:
|
| 18 |
+
print('the value inside `-characters cannot represent a math function')
|
| 19 |
+
return 1
|
| 20 |
+
return float(numexpr.evaluate(w_raw[1:-1]))
|
| 21 |
+
|
| 22 |
+
def split_weighted_subprompts(text, frame=0, max_frames=0):
|
| 23 |
+
"""
|
| 24 |
+
splits the prompt based on deforum webui implementation, moved from generate.py
|
| 25 |
+
"""
|
| 26 |
+
math_parser = re.compile("(?P<weight>(`[\S\s]*?`))", re.VERBOSE)
|
| 27 |
+
|
| 28 |
+
parsed_prompt = re.sub(math_parser, lambda m: str(parse_weight(m, frame)), text)
|
| 29 |
+
|
| 30 |
+
negative_prompts = []
|
| 31 |
+
positive_prompts = []
|
| 32 |
+
|
| 33 |
+
prompt_split = parsed_prompt.split("--neg")
|
| 34 |
+
if len(prompt_split) > 1:
|
| 35 |
+
positive_prompts, negative_prompts = parsed_prompt.split("--neg") # TODO: add --neg to vanilla Deforum for compat
|
| 36 |
+
else:
|
| 37 |
+
positive_prompts = prompt_split[0]
|
| 38 |
+
negative_prompts = ""
|
| 39 |
+
|
| 40 |
+
return positive_prompts, negative_prompts
|
| 41 |
+
|
| 42 |
+
def interpolate_prompts(animation_prompts, max_frames):
|
| 43 |
+
import numpy as np
|
| 44 |
+
import pandas as pd
|
| 45 |
+
# Get prompts sorted by keyframe
|
| 46 |
+
max_f = max_frames
|
| 47 |
+
parsed_animation_prompts = {}
|
| 48 |
+
for key, value in animation_prompts.items():
|
| 49 |
+
if check_is_number(key): # default case 0:(1 + t %5), 30:(5-t%2)
|
| 50 |
+
parsed_animation_prompts[key] = value
|
| 51 |
+
else: # math on the left hand side case 0:(1 + t %5), maxKeyframes/2:(5-t%2)
|
| 52 |
+
parsed_animation_prompts[int(numexpr.evaluate(key))] = value
|
| 53 |
+
|
| 54 |
+
sorted_prompts = sorted(parsed_animation_prompts.items(), key=lambda item: int(item[0]))
|
| 55 |
+
|
| 56 |
+
# Setup container for interpolated prompts
|
| 57 |
+
prompt_series = pd.Series([np.nan for a in range(max_frames)])
|
| 58 |
+
|
| 59 |
+
# For every keyframe prompt except the last
|
| 60 |
+
for i in range(0, len(sorted_prompts) - 1):
|
| 61 |
+
# Get current and next keyframe
|
| 62 |
+
current_frame = int(sorted_prompts[i][0])
|
| 63 |
+
next_frame = int(sorted_prompts[i + 1][0])
|
| 64 |
+
|
| 65 |
+
# Ensure there's no weird ordering issues or duplication in the animation prompts
|
| 66 |
+
# (unlikely because we sort above, and the json parser will strip dupes)
|
| 67 |
+
if current_frame >= next_frame:
|
| 68 |
+
print(f"WARNING: Sequential prompt keyframes {i}:{current_frame} and {i + 1}:{next_frame} are not monotonously increasing; skipping interpolation.")
|
| 69 |
+
continue
|
| 70 |
+
|
| 71 |
+
# Get current and next keyframes' positive and negative prompts (if any)
|
| 72 |
+
current_prompt = sorted_prompts[i][1]
|
| 73 |
+
next_prompt = sorted_prompts[i + 1][1]
|
| 74 |
+
current_positive, current_negative, *_ = current_prompt.split("--neg") + [None]
|
| 75 |
+
next_positive, next_negative, *_ = next_prompt.split("--neg") + [None]
|
| 76 |
+
# Calculate how much to shift the weight from current to next prompt at each frame
|
| 77 |
+
weight_step = 1 / (next_frame - current_frame)
|
| 78 |
+
|
| 79 |
+
# Apply weighted prompt interpolation for each frame between current and next keyframe
|
| 80 |
+
# using the syntax: prompt1 :weight1 AND prompt1 :weight2 --neg nprompt1 :weight1 AND nprompt1 :weight2
|
| 81 |
+
# (See: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#composable-diffusion )
|
| 82 |
+
for f in range(current_frame, next_frame):
|
| 83 |
+
next_weight = weight_step * (f - current_frame)
|
| 84 |
+
current_weight = 1 - next_weight
|
| 85 |
+
|
| 86 |
+
# We will build the prompt incrementally depending on which prompts are present
|
| 87 |
+
prompt_series[f] = ''
|
| 88 |
+
|
| 89 |
+
# Cater for the case where neither, either or both current & next have positive prompts:
|
| 90 |
+
if current_positive:
|
| 91 |
+
prompt_series[f] += f" ({current_positive}):{current_weight}"
|
| 92 |
+
if current_positive and next_positive:
|
| 93 |
+
prompt_series[f] += f" AND "
|
| 94 |
+
if next_positive:
|
| 95 |
+
prompt_series[f] += f" ({next_positive}):{next_weight}"
|
| 96 |
+
|
| 97 |
+
# Cater for the case where neither, either or both current & next have negative prompts:
|
| 98 |
+
if len(current_negative) > 1 or len(next_negative) > 1:
|
| 99 |
+
prompt_series[f] += " --neg "
|
| 100 |
+
if len(current_negative) > 1:
|
| 101 |
+
prompt_series[f] += f" ({current_negative}):{current_weight}"
|
| 102 |
+
if len(current_negative) > 1 and len(next_negative) > 1:
|
| 103 |
+
prompt_series[f] += f" AND "
|
| 104 |
+
if len(next_negative) > 1:
|
| 105 |
+
prompt_series[f] += f" ({next_negative}):{next_weight}"
|
| 106 |
+
|
| 107 |
+
# Set explicitly declared keyframe prompts (overwriting interpolated values at the keyframe idx). This ensures:
|
| 108 |
+
# - That final prompt is set, and
|
| 109 |
+
# - Gives us a chance to emit warnings if any keyframe prompts are already using composable diffusion
|
| 110 |
+
for i, prompt in parsed_animation_prompts.items():
|
| 111 |
+
prompt_series[int(i)] = prompt
|
| 112 |
+
if ' AND ' in prompt:
|
| 113 |
+
print(f"WARNING: keyframe {i}'s prompt is using composable diffusion (aka the 'AND' keyword). This will cause unexpected behaviour with interpolation.")
|
| 114 |
+
|
| 115 |
+
# Return the filled series, in case max_frames is greater than the last keyframe or any ranges were skipped.
|
| 116 |
+
return prompt_series.ffill().bfill()
|
| 117 |
+
|
| 118 |
+
def prepare_prompt(prompt_series, max_frames, seed, frame_idx):
|
| 119 |
+
max_f = max_frames - 1
|
| 120 |
+
pattern = r'`.*?`'
|
| 121 |
+
regex = re.compile(pattern)
|
| 122 |
+
prompt_parsed = prompt_series
|
| 123 |
+
for match in regex.finditer(prompt_parsed):
|
| 124 |
+
matched_string = match.group(0)
|
| 125 |
+
parsed_string = matched_string.replace('t', f'{frame_idx}').replace("max_f", f"{max_f}").replace('`', '')
|
| 126 |
+
parsed_value = numexpr.evaluate(parsed_string)
|
| 127 |
+
prompt_parsed = prompt_parsed.replace(matched_string, str(parsed_value))
|
| 128 |
+
|
| 129 |
+
prompt_to_print, *after_neg = prompt_parsed.strip().split("--neg")
|
| 130 |
+
prompt_to_print = prompt_to_print.strip()
|
| 131 |
+
after_neg = "".join(after_neg).strip()
|
| 132 |
+
|
| 133 |
+
print(f"\033[32mSeed: \033[0m{seed}")
|
| 134 |
+
print(f"\033[35mPrompt: \033[0m{prompt_to_print}")
|
| 135 |
+
if after_neg and after_neg.strip():
|
| 136 |
+
print(f"\033[91mNeg Prompt: \033[0m{after_neg}")
|
| 137 |
+
prompt_to_print += f"--neg {after_neg}"
|
| 138 |
+
|
| 139 |
+
# set value back into the prompt
|
| 140 |
+
return prompt_to_print
|
scripts/deforum_helpers/render.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
import numexpr
|
| 6 |
+
import gc
|
| 7 |
+
import random
|
| 8 |
+
import PIL
|
| 9 |
+
import time
|
| 10 |
+
from PIL import Image, ImageOps
|
| 11 |
+
from .generate import generate, isJson
|
| 12 |
+
from .noise import add_noise
|
| 13 |
+
from .animation import anim_frame_warp
|
| 14 |
+
from .animation_key_frames import DeformAnimKeys, LooperAnimKeys
|
| 15 |
+
from .video_audio_utilities import get_frame_name, get_next_frame
|
| 16 |
+
from .depth import DepthModel
|
| 17 |
+
from .colors import maintain_colors
|
| 18 |
+
from .parseq_adapter import ParseqAdapter
|
| 19 |
+
from .seed import next_seed
|
| 20 |
+
from .image_sharpening import unsharp_mask
|
| 21 |
+
from .load_images import get_mask, load_img, load_image, get_mask_from_file
|
| 22 |
+
from .hybrid_video import (
|
| 23 |
+
hybrid_generation, hybrid_composite, get_matrix_for_hybrid_motion, get_matrix_for_hybrid_motion_prev, get_flow_for_hybrid_motion, get_flow_for_hybrid_motion_prev, image_transform_ransac,
|
| 24 |
+
image_transform_optical_flow, get_flow_from_images, abs_flow_to_rel_flow, rel_flow_to_abs_flow)
|
| 25 |
+
from .save_images import save_image
|
| 26 |
+
from .composable_masks import compose_mask_with_check
|
| 27 |
+
from .settings import save_settings_from_animation_run
|
| 28 |
+
from .deforum_controlnet import unpack_controlnet_vids, is_controlnet_enabled
|
| 29 |
+
from .subtitle_handler import init_srt_file, write_frame_subtitle, format_animation_params
|
| 30 |
+
from .resume import get_resume_vars
|
| 31 |
+
from .masks import do_overlay_mask
|
| 32 |
+
from .prompt import prepare_prompt
|
| 33 |
+
from modules.shared import opts, cmd_opts, state, sd_model
|
| 34 |
+
from modules import lowvram, devices, sd_hijack
|
| 35 |
+
from .RAFT import RAFT
|
| 36 |
+
|
| 37 |
+
def render_animation(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root):
|
| 38 |
+
if opts.data.get("deforum_save_gen_info_as_srt", False): # create .srt file and set timeframe mechanism using FPS
|
| 39 |
+
srt_filename = os.path.join(args.outdir, f"{root.timestring}.srt")
|
| 40 |
+
srt_frame_duration = init_srt_file(srt_filename, video_args.fps)
|
| 41 |
+
|
| 42 |
+
if anim_args.animation_mode in ['2D', '3D']:
|
| 43 |
+
# handle hybrid video generation
|
| 44 |
+
if anim_args.hybrid_composite != 'None' or anim_args.hybrid_motion in ['Affine', 'Perspective', 'Optical Flow']:
|
| 45 |
+
args, anim_args, inputfiles = hybrid_generation(args, anim_args, root)
|
| 46 |
+
# path required by hybrid functions, even if hybrid_comp_save_extra_frames is False
|
| 47 |
+
hybrid_frame_path = os.path.join(args.outdir, 'hybridframes')
|
| 48 |
+
# initialize prev_flow
|
| 49 |
+
if anim_args.hybrid_motion == 'Optical Flow':
|
| 50 |
+
prev_flow = None
|
| 51 |
+
|
| 52 |
+
if loop_args.use_looper:
|
| 53 |
+
print("Using Guided Images mode: seed_behavior will be set to 'schedule' and 'strength_0_no_init' to False")
|
| 54 |
+
if args.strength == 0:
|
| 55 |
+
raise RuntimeError("Strength needs to be greater than 0 in Init tab")
|
| 56 |
+
args.strength_0_no_init = False
|
| 57 |
+
args.seed_behavior = "schedule"
|
| 58 |
+
if not isJson(loop_args.init_images):
|
| 59 |
+
raise RuntimeError("The images set for use with keyframe-guidance are not in a proper JSON format")
|
| 60 |
+
|
| 61 |
+
# handle controlnet video input frames generation
|
| 62 |
+
if is_controlnet_enabled(controlnet_args):
|
| 63 |
+
unpack_controlnet_vids(args, anim_args, controlnet_args)
|
| 64 |
+
|
| 65 |
+
# initialise Parseq adapter
|
| 66 |
+
parseq_adapter = ParseqAdapter(parseq_args, anim_args, video_args, controlnet_args)
|
| 67 |
+
|
| 68 |
+
# expand key frame strings to values
|
| 69 |
+
keys = DeformAnimKeys(anim_args, args.seed) if not parseq_adapter.use_parseq else parseq_adapter.anim_keys
|
| 70 |
+
loopSchedulesAndData = LooperAnimKeys(loop_args, anim_args, args.seed)
|
| 71 |
+
|
| 72 |
+
# create output folder for the batch
|
| 73 |
+
os.makedirs(args.outdir, exist_ok=True)
|
| 74 |
+
print(f"Saving animation frames to:\n{args.outdir}")
|
| 75 |
+
|
| 76 |
+
# save settings.txt file for the current run
|
| 77 |
+
save_settings_from_animation_run(args, anim_args, parseq_args, loop_args, controlnet_args, video_args, root)
|
| 78 |
+
|
| 79 |
+
# resume from timestring
|
| 80 |
+
if anim_args.resume_from_timestring:
|
| 81 |
+
root.timestring = anim_args.resume_timestring
|
| 82 |
+
|
| 83 |
+
# Always enable pseudo-3d with parseq. No need for an extra toggle:
|
| 84 |
+
# Whether it's used or not in practice is defined by the schedules
|
| 85 |
+
if parseq_adapter.use_parseq:
|
| 86 |
+
anim_args.flip_2d_perspective = True
|
| 87 |
+
|
| 88 |
+
# expand prompts out to per-frame
|
| 89 |
+
if parseq_adapter.manages_prompts():
|
| 90 |
+
prompt_series = keys.prompts
|
| 91 |
+
else:
|
| 92 |
+
prompt_series = pd.Series([np.nan for a in range(anim_args.max_frames)])
|
| 93 |
+
for i, prompt in root.animation_prompts.items():
|
| 94 |
+
if str(i).isdigit():
|
| 95 |
+
prompt_series[int(i)] = prompt
|
| 96 |
+
else:
|
| 97 |
+
prompt_series[int(numexpr.evaluate(i))] = prompt
|
| 98 |
+
prompt_series = prompt_series.ffill().bfill()
|
| 99 |
+
|
| 100 |
+
# check for video inits
|
| 101 |
+
using_vid_init = anim_args.animation_mode == 'Video Input'
|
| 102 |
+
|
| 103 |
+
# load depth model for 3D
|
| 104 |
+
predict_depths = (anim_args.animation_mode == '3D' and anim_args.use_depth_warping) or anim_args.save_depth_maps
|
| 105 |
+
predict_depths = predict_depths or (anim_args.hybrid_composite and anim_args.hybrid_comp_mask_type in ['Depth', 'Video Depth'])
|
| 106 |
+
if predict_depths:
|
| 107 |
+
keep_in_vram = opts.data.get("deforum_keep_3d_models_in_vram")
|
| 108 |
+
|
| 109 |
+
device = ('cpu' if cmd_opts.lowvram or cmd_opts.medvram else root.device)
|
| 110 |
+
depth_model = DepthModel(root.models_path, device, root.half_precision, keep_in_vram=keep_in_vram, depth_algorithm=anim_args.depth_algorithm, Width=args.W, Height=args.H,
|
| 111 |
+
midas_weight=anim_args.midas_weight)
|
| 112 |
+
|
| 113 |
+
# depth-based hybrid composite mask requires saved depth maps
|
| 114 |
+
if anim_args.hybrid_composite != 'None' and anim_args.hybrid_comp_mask_type == 'Depth':
|
| 115 |
+
anim_args.save_depth_maps = True
|
| 116 |
+
else:
|
| 117 |
+
depth_model = None
|
| 118 |
+
anim_args.save_depth_maps = False
|
| 119 |
+
|
| 120 |
+
raft_model = None
|
| 121 |
+
load_raft = (anim_args.optical_flow_cadence == "RAFT" and int(anim_args.diffusion_cadence) > 1) or \
|
| 122 |
+
(anim_args.hybrid_motion == "Optical Flow" and anim_args.hybrid_flow_method == "RAFT") or \
|
| 123 |
+
(anim_args.optical_flow_redo_generation == "RAFT")
|
| 124 |
+
if load_raft:
|
| 125 |
+
print("Loading RAFT model...")
|
| 126 |
+
raft_model = RAFT()
|
| 127 |
+
|
| 128 |
+
# state for interpolating between diffusion steps
|
| 129 |
+
turbo_steps = 1 if using_vid_init else int(anim_args.diffusion_cadence)
|
| 130 |
+
turbo_prev_image, turbo_prev_frame_idx = None, 0
|
| 131 |
+
turbo_next_image, turbo_next_frame_idx = None, 0
|
| 132 |
+
|
| 133 |
+
# initialize vars
|
| 134 |
+
prev_img = None
|
| 135 |
+
color_match_sample = None
|
| 136 |
+
start_frame = 0
|
| 137 |
+
|
| 138 |
+
# resume animation (requires at least two frames - see function)
|
| 139 |
+
if anim_args.resume_from_timestring:
|
| 140 |
+
# determine last frame and frame to start on
|
| 141 |
+
prev_frame, next_frame, prev_img, next_img = get_resume_vars(
|
| 142 |
+
folder=args.outdir,
|
| 143 |
+
timestring=anim_args.resume_timestring,
|
| 144 |
+
cadence=turbo_steps
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# set up turbo step vars
|
| 148 |
+
if turbo_steps > 1:
|
| 149 |
+
turbo_prev_image, turbo_prev_frame_idx = prev_img, prev_frame
|
| 150 |
+
turbo_next_image, turbo_next_frame_idx = next_img, next_frame
|
| 151 |
+
|
| 152 |
+
# advance start_frame to next frame
|
| 153 |
+
start_frame = next_frame + 1
|
| 154 |
+
|
| 155 |
+
frame_idx = start_frame
|
| 156 |
+
|
| 157 |
+
# reset the mask vals as they are overwritten in the compose_mask algorithm
|
| 158 |
+
mask_vals = {}
|
| 159 |
+
noise_mask_vals = {}
|
| 160 |
+
|
| 161 |
+
mask_vals['everywhere'] = Image.new('1', (args.W, args.H), 1)
|
| 162 |
+
noise_mask_vals['everywhere'] = Image.new('1', (args.W, args.H), 1)
|
| 163 |
+
|
| 164 |
+
mask_image = None
|
| 165 |
+
|
| 166 |
+
if args.use_init and args.init_image != None and args.init_image != '':
|
| 167 |
+
_, mask_image = load_img(args.init_image,
|
| 168 |
+
shape=(args.W, args.H),
|
| 169 |
+
use_alpha_as_mask=args.use_alpha_as_mask)
|
| 170 |
+
mask_vals['video_mask'] = mask_image
|
| 171 |
+
noise_mask_vals['video_mask'] = mask_image
|
| 172 |
+
|
| 173 |
+
# Grab the first frame masks since they wont be provided until next frame
|
| 174 |
+
# Video mask overrides the init image mask, also, won't be searching for init_mask if use_mask_video is set
|
| 175 |
+
# Made to solve https://github.com/deforum-art/deforum-for-automatic1111-webui/issues/386
|
| 176 |
+
if anim_args.use_mask_video:
|
| 177 |
+
|
| 178 |
+
args.mask_file = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 179 |
+
root.noise_mask = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 180 |
+
|
| 181 |
+
mask_vals['video_mask'] = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 182 |
+
noise_mask_vals['video_mask'] = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 183 |
+
elif mask_image is None and args.use_mask:
|
| 184 |
+
mask_vals['video_mask'] = get_mask(args)
|
| 185 |
+
noise_mask_vals['video_mask'] = get_mask(args) # TODO?: add a different default noisc mask
|
| 186 |
+
|
| 187 |
+
# get color match for 'Image' color coherence only once, before loop
|
| 188 |
+
if anim_args.color_coherence == 'Image':
|
| 189 |
+
color_match_sample = load_image(anim_args.color_coherence_image_path)
|
| 190 |
+
color_match_sample = color_match_sample.resize((args.W, args.H), PIL.Image.LANCZOS)
|
| 191 |
+
color_match_sample = cv2.cvtColor(np.array(color_match_sample), cv2.COLOR_RGB2BGR)
|
| 192 |
+
|
| 193 |
+
# Webui
|
| 194 |
+
state.job_count = anim_args.max_frames
|
| 195 |
+
|
| 196 |
+
while frame_idx < anim_args.max_frames:
|
| 197 |
+
# Webui
|
| 198 |
+
|
| 199 |
+
state.job = f"frame {frame_idx + 1}/{anim_args.max_frames}"
|
| 200 |
+
state.job_no = frame_idx + 1
|
| 201 |
+
|
| 202 |
+
if state.skipped:
|
| 203 |
+
print("\n** PAUSED **")
|
| 204 |
+
state.skipped = False
|
| 205 |
+
while not state.skipped:
|
| 206 |
+
time.sleep(0.1)
|
| 207 |
+
print("** RESUMING **")
|
| 208 |
+
|
| 209 |
+
print(f"\033[36mAnimation frame: \033[0m{frame_idx}/{anim_args.max_frames} ")
|
| 210 |
+
|
| 211 |
+
noise = keys.noise_schedule_series[frame_idx]
|
| 212 |
+
strength = keys.strength_schedule_series[frame_idx]
|
| 213 |
+
scale = keys.cfg_scale_schedule_series[frame_idx]
|
| 214 |
+
contrast = keys.contrast_schedule_series[frame_idx]
|
| 215 |
+
kernel = int(keys.kernel_schedule_series[frame_idx])
|
| 216 |
+
sigma = keys.sigma_schedule_series[frame_idx]
|
| 217 |
+
amount = keys.amount_schedule_series[frame_idx]
|
| 218 |
+
threshold = keys.threshold_schedule_series[frame_idx]
|
| 219 |
+
cadence_flow_factor = keys.cadence_flow_factor_schedule_series[frame_idx]
|
| 220 |
+
redo_flow_factor = keys.redo_flow_factor_schedule_series[frame_idx]
|
| 221 |
+
hybrid_comp_schedules = {
|
| 222 |
+
"alpha": keys.hybrid_comp_alpha_schedule_series[frame_idx],
|
| 223 |
+
"mask_blend_alpha": keys.hybrid_comp_mask_blend_alpha_schedule_series[frame_idx],
|
| 224 |
+
"mask_contrast": keys.hybrid_comp_mask_contrast_schedule_series[frame_idx],
|
| 225 |
+
"mask_auto_contrast_cutoff_low": int(keys.hybrid_comp_mask_auto_contrast_cutoff_low_schedule_series[frame_idx]),
|
| 226 |
+
"mask_auto_contrast_cutoff_high": int(keys.hybrid_comp_mask_auto_contrast_cutoff_high_schedule_series[frame_idx]),
|
| 227 |
+
"flow_factor": keys.hybrid_flow_factor_schedule_series[frame_idx]
|
| 228 |
+
}
|
| 229 |
+
scheduled_sampler_name = None
|
| 230 |
+
scheduled_clipskip = None
|
| 231 |
+
scheduled_noise_multiplier = None
|
| 232 |
+
scheduled_ddim_eta = None
|
| 233 |
+
scheduled_ancestral_eta = None
|
| 234 |
+
|
| 235 |
+
mask_seq = None
|
| 236 |
+
noise_mask_seq = None
|
| 237 |
+
if anim_args.enable_steps_scheduling and keys.steps_schedule_series[frame_idx] is not None:
|
| 238 |
+
args.steps = int(keys.steps_schedule_series[frame_idx])
|
| 239 |
+
if anim_args.enable_sampler_scheduling and keys.sampler_schedule_series[frame_idx] is not None:
|
| 240 |
+
scheduled_sampler_name = keys.sampler_schedule_series[frame_idx].casefold()
|
| 241 |
+
if anim_args.enable_clipskip_scheduling and keys.clipskip_schedule_series[frame_idx] is not None:
|
| 242 |
+
scheduled_clipskip = int(keys.clipskip_schedule_series[frame_idx])
|
| 243 |
+
if anim_args.enable_noise_multiplier_scheduling and keys.noise_multiplier_schedule_series[frame_idx] is not None:
|
| 244 |
+
scheduled_noise_multiplier = float(keys.noise_multiplier_schedule_series[frame_idx])
|
| 245 |
+
if anim_args.enable_ddim_eta_scheduling and keys.ddim_eta_schedule_series[frame_idx] is not None:
|
| 246 |
+
scheduled_ddim_eta = float(keys.ddim_eta_schedule_series[frame_idx])
|
| 247 |
+
if anim_args.enable_ancestral_eta_scheduling and keys.ancestral_eta_schedule_series[frame_idx] is not None:
|
| 248 |
+
scheduled_ancestral_eta = float(keys.ancestral_eta_schedule_series[frame_idx])
|
| 249 |
+
if args.use_mask and keys.mask_schedule_series[frame_idx] is not None:
|
| 250 |
+
mask_seq = keys.mask_schedule_series[frame_idx]
|
| 251 |
+
if anim_args.use_noise_mask and keys.noise_mask_schedule_series[frame_idx] is not None:
|
| 252 |
+
noise_mask_seq = keys.noise_mask_schedule_series[frame_idx]
|
| 253 |
+
|
| 254 |
+
if args.use_mask and not anim_args.use_noise_mask:
|
| 255 |
+
noise_mask_seq = mask_seq
|
| 256 |
+
|
| 257 |
+
depth = None
|
| 258 |
+
|
| 259 |
+
if anim_args.animation_mode == '3D' and (cmd_opts.lowvram or cmd_opts.medvram):
|
| 260 |
+
# Unload the main checkpoint and load the depth model
|
| 261 |
+
lowvram.send_everything_to_cpu()
|
| 262 |
+
sd_hijack.model_hijack.undo_hijack(sd_model)
|
| 263 |
+
devices.torch_gc()
|
| 264 |
+
if predict_depths: depth_model.to(root.device)
|
| 265 |
+
|
| 266 |
+
if turbo_steps == 1 and opts.data.get("deforum_save_gen_info_as_srt"):
|
| 267 |
+
params_string = format_animation_params(keys, prompt_series, frame_idx)
|
| 268 |
+
write_frame_subtitle(srt_filename, frame_idx, srt_frame_duration, f"F#: {frame_idx}; Cadence: false; Seed: {args.seed}; {params_string}")
|
| 269 |
+
params_string = None
|
| 270 |
+
|
| 271 |
+
# emit in-between frames
|
| 272 |
+
if turbo_steps > 1:
|
| 273 |
+
tween_frame_start_idx = max(start_frame, frame_idx - turbo_steps)
|
| 274 |
+
cadence_flow = None
|
| 275 |
+
for tween_frame_idx in range(tween_frame_start_idx, frame_idx):
|
| 276 |
+
# update progress during cadence
|
| 277 |
+
state.job = f"frame {tween_frame_idx + 1}/{anim_args.max_frames}"
|
| 278 |
+
state.job_no = tween_frame_idx + 1
|
| 279 |
+
# cadence vars
|
| 280 |
+
tween = float(tween_frame_idx - tween_frame_start_idx + 1) / float(frame_idx - tween_frame_start_idx)
|
| 281 |
+
advance_prev = turbo_prev_image is not None and tween_frame_idx > turbo_prev_frame_idx
|
| 282 |
+
advance_next = tween_frame_idx > turbo_next_frame_idx
|
| 283 |
+
|
| 284 |
+
# optical flow cadence setup before animation warping
|
| 285 |
+
if anim_args.animation_mode in ['2D', '3D'] and anim_args.optical_flow_cadence != 'None':
|
| 286 |
+
if keys.strength_schedule_series[tween_frame_start_idx] > 0:
|
| 287 |
+
if cadence_flow is None and turbo_prev_image is not None and turbo_next_image is not None:
|
| 288 |
+
cadence_flow = get_flow_from_images(turbo_prev_image, turbo_next_image, anim_args.optical_flow_cadence, raft_model) / 2
|
| 289 |
+
turbo_next_image = image_transform_optical_flow(turbo_next_image, -cadence_flow, 1)
|
| 290 |
+
|
| 291 |
+
if opts.data.get("deforum_save_gen_info_as_srt"):
|
| 292 |
+
params_string = format_animation_params(keys, prompt_series, tween_frame_idx)
|
| 293 |
+
write_frame_subtitle(srt_filename, tween_frame_idx, srt_frame_duration, f"F#: {tween_frame_idx}; Cadence: {tween < 1.0}; Seed: {args.seed}; {params_string}")
|
| 294 |
+
params_string = None
|
| 295 |
+
|
| 296 |
+
print(f"Creating in-between {'' if cadence_flow is None else anim_args.optical_flow_cadence + ' optical flow '}cadence frame: {tween_frame_idx}; tween:{tween:0.2f};")
|
| 297 |
+
|
| 298 |
+
if depth_model is not None:
|
| 299 |
+
assert (turbo_next_image is not None)
|
| 300 |
+
depth = depth_model.predict(turbo_next_image, anim_args.midas_weight, root.half_precision)
|
| 301 |
+
|
| 302 |
+
if advance_prev:
|
| 303 |
+
turbo_prev_image, _ = anim_frame_warp(turbo_prev_image, args, anim_args, keys, tween_frame_idx, depth_model, depth=depth, device=root.device, half_precision=root.half_precision)
|
| 304 |
+
if advance_next:
|
| 305 |
+
turbo_next_image, _ = anim_frame_warp(turbo_next_image, args, anim_args, keys, tween_frame_idx, depth_model, depth=depth, device=root.device, half_precision=root.half_precision)
|
| 306 |
+
|
| 307 |
+
# hybrid video motion - warps turbo_prev_image or turbo_next_image to match motion
|
| 308 |
+
if tween_frame_idx > 0:
|
| 309 |
+
if anim_args.hybrid_motion in ['Affine', 'Perspective']:
|
| 310 |
+
if anim_args.hybrid_motion_use_prev_img:
|
| 311 |
+
matrix = get_matrix_for_hybrid_motion_prev(tween_frame_idx - 1, (args.W, args.H), inputfiles, prev_img, anim_args.hybrid_motion)
|
| 312 |
+
if advance_prev:
|
| 313 |
+
turbo_prev_image = image_transform_ransac(turbo_prev_image, matrix, anim_args.hybrid_motion)
|
| 314 |
+
if advance_next:
|
| 315 |
+
turbo_next_image = image_transform_ransac(turbo_next_image, matrix, anim_args.hybrid_motion)
|
| 316 |
+
else:
|
| 317 |
+
matrix = get_matrix_for_hybrid_motion(tween_frame_idx - 1, (args.W, args.H), inputfiles, anim_args.hybrid_motion)
|
| 318 |
+
if advance_prev:
|
| 319 |
+
turbo_prev_image = image_transform_ransac(turbo_prev_image, matrix, anim_args.hybrid_motion)
|
| 320 |
+
if advance_next:
|
| 321 |
+
turbo_next_image = image_transform_ransac(turbo_next_image, matrix, anim_args.hybrid_motion)
|
| 322 |
+
if anim_args.hybrid_motion in ['Optical Flow']:
|
| 323 |
+
if anim_args.hybrid_motion_use_prev_img:
|
| 324 |
+
flow = get_flow_for_hybrid_motion_prev(tween_frame_idx - 1, (args.W, args.H), inputfiles, hybrid_frame_path, prev_flow, prev_img, anim_args.hybrid_flow_method, raft_model,
|
| 325 |
+
anim_args.hybrid_flow_consistency, anim_args.hybrid_consistency_blur, anim_args.hybrid_comp_save_extra_frames)
|
| 326 |
+
if advance_prev:
|
| 327 |
+
turbo_prev_image = image_transform_optical_flow(turbo_prev_image, flow, hybrid_comp_schedules['flow_factor'])
|
| 328 |
+
if advance_next:
|
| 329 |
+
turbo_next_image = image_transform_optical_flow(turbo_next_image, flow, hybrid_comp_schedules['flow_factor'])
|
| 330 |
+
prev_flow = flow
|
| 331 |
+
else:
|
| 332 |
+
flow = get_flow_for_hybrid_motion(tween_frame_idx - 1, (args.W, args.H), inputfiles, hybrid_frame_path, prev_flow, anim_args.hybrid_flow_method, raft_model,
|
| 333 |
+
anim_args.hybrid_flow_consistency, anim_args.hybrid_consistency_blur, anim_args.hybrid_comp_save_extra_frames)
|
| 334 |
+
if advance_prev:
|
| 335 |
+
turbo_prev_image = image_transform_optical_flow(turbo_prev_image, flow, hybrid_comp_schedules['flow_factor'])
|
| 336 |
+
if advance_next:
|
| 337 |
+
turbo_next_image = image_transform_optical_flow(turbo_next_image, flow, hybrid_comp_schedules['flow_factor'])
|
| 338 |
+
prev_flow = flow
|
| 339 |
+
|
| 340 |
+
# do optical flow cadence after animation warping
|
| 341 |
+
if cadence_flow is not None:
|
| 342 |
+
cadence_flow = abs_flow_to_rel_flow(cadence_flow, args.W, args.H)
|
| 343 |
+
cadence_flow, _ = anim_frame_warp(cadence_flow, args, anim_args, keys, tween_frame_idx, depth_model, depth=depth, device=root.device, half_precision=root.half_precision)
|
| 344 |
+
cadence_flow_inc = rel_flow_to_abs_flow(cadence_flow, args.W, args.H) * tween
|
| 345 |
+
if advance_prev:
|
| 346 |
+
turbo_prev_image = image_transform_optical_flow(turbo_prev_image, cadence_flow_inc, cadence_flow_factor)
|
| 347 |
+
if advance_next:
|
| 348 |
+
turbo_next_image = image_transform_optical_flow(turbo_next_image, cadence_flow_inc, cadence_flow_factor)
|
| 349 |
+
|
| 350 |
+
turbo_prev_frame_idx = turbo_next_frame_idx = tween_frame_idx
|
| 351 |
+
|
| 352 |
+
if turbo_prev_image is not None and tween < 1.0:
|
| 353 |
+
img = turbo_prev_image * (1.0 - tween) + turbo_next_image * tween
|
| 354 |
+
else:
|
| 355 |
+
img = turbo_next_image
|
| 356 |
+
|
| 357 |
+
# intercept and override to grayscale
|
| 358 |
+
if anim_args.color_force_grayscale:
|
| 359 |
+
img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY)
|
| 360 |
+
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
| 361 |
+
|
| 362 |
+
# overlay mask
|
| 363 |
+
if args.overlay_mask and (anim_args.use_mask_video or args.use_mask):
|
| 364 |
+
img = do_overlay_mask(args, anim_args, img, tween_frame_idx, True)
|
| 365 |
+
|
| 366 |
+
# get prev_img during cadence
|
| 367 |
+
prev_img = img
|
| 368 |
+
|
| 369 |
+
# current image update for cadence frames (left commented because it doesn't currently update the preview)
|
| 370 |
+
# state.current_image = Image.fromarray(cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2RGB))
|
| 371 |
+
|
| 372 |
+
# saving cadence frames
|
| 373 |
+
filename = f"{root.timestring}_{tween_frame_idx:09}.png"
|
| 374 |
+
cv2.imwrite(os.path.join(args.outdir, filename), img)
|
| 375 |
+
if anim_args.save_depth_maps:
|
| 376 |
+
depth_model.save(os.path.join(args.outdir, f"{root.timestring}_depth_{tween_frame_idx:09}.png"), depth)
|
| 377 |
+
|
| 378 |
+
# get color match for video outside of prev_img conditional
|
| 379 |
+
hybrid_available = anim_args.hybrid_composite != 'None' or anim_args.hybrid_motion in ['Optical Flow', 'Affine', 'Perspective']
|
| 380 |
+
if anim_args.color_coherence == 'Video Input' and hybrid_available:
|
| 381 |
+
if int(frame_idx) % int(anim_args.color_coherence_video_every_N_frames) == 0:
|
| 382 |
+
prev_vid_img = Image.open(os.path.join(args.outdir, 'inputframes', get_frame_name(anim_args.video_init_path) + f"{frame_idx:09}.jpg"))
|
| 383 |
+
prev_vid_img = prev_vid_img.resize((args.W, args.H), PIL.Image.LANCZOS)
|
| 384 |
+
color_match_sample = np.asarray(prev_vid_img)
|
| 385 |
+
color_match_sample = cv2.cvtColor(color_match_sample, cv2.COLOR_RGB2BGR)
|
| 386 |
+
|
| 387 |
+
# after 1st frame, prev_img exists
|
| 388 |
+
if prev_img is not None:
|
| 389 |
+
# apply transforms to previous frame
|
| 390 |
+
prev_img, depth = anim_frame_warp(prev_img, args, anim_args, keys, frame_idx, depth_model, depth=None, device=root.device, half_precision=root.half_precision)
|
| 391 |
+
|
| 392 |
+
# do hybrid compositing before motion
|
| 393 |
+
if anim_args.hybrid_composite == 'Before Motion':
|
| 394 |
+
args, prev_img = hybrid_composite(args, anim_args, frame_idx, prev_img, depth_model, hybrid_comp_schedules, root)
|
| 395 |
+
|
| 396 |
+
# hybrid video motion - warps prev_img to match motion, usually to prepare for compositing
|
| 397 |
+
if anim_args.hybrid_motion in ['Affine', 'Perspective']:
|
| 398 |
+
if anim_args.hybrid_motion_use_prev_img:
|
| 399 |
+
matrix = get_matrix_for_hybrid_motion_prev(frame_idx - 1, (args.W, args.H), inputfiles, prev_img, anim_args.hybrid_motion)
|
| 400 |
+
else:
|
| 401 |
+
matrix = get_matrix_for_hybrid_motion(frame_idx - 1, (args.W, args.H), inputfiles, anim_args.hybrid_motion)
|
| 402 |
+
prev_img = image_transform_ransac(prev_img, matrix, anim_args.hybrid_motion)
|
| 403 |
+
if anim_args.hybrid_motion in ['Optical Flow']:
|
| 404 |
+
if anim_args.hybrid_motion_use_prev_img:
|
| 405 |
+
flow = get_flow_for_hybrid_motion_prev(frame_idx - 1, (args.W, args.H), inputfiles, hybrid_frame_path, prev_flow, prev_img, anim_args.hybrid_flow_method, raft_model,
|
| 406 |
+
anim_args.hybrid_flow_consistency, anim_args.hybrid_consistency_blur, anim_args.hybrid_comp_save_extra_frames)
|
| 407 |
+
else:
|
| 408 |
+
flow = get_flow_for_hybrid_motion(frame_idx - 1, (args.W, args.H), inputfiles, hybrid_frame_path, prev_flow, anim_args.hybrid_flow_method, raft_model,
|
| 409 |
+
anim_args.hybrid_flow_consistency, anim_args.hybrid_consistency_blur, anim_args.hybrid_comp_save_extra_frames)
|
| 410 |
+
prev_img = image_transform_optical_flow(prev_img, flow, hybrid_comp_schedules['flow_factor'])
|
| 411 |
+
prev_flow = flow
|
| 412 |
+
|
| 413 |
+
# do hybrid compositing after motion (normal)
|
| 414 |
+
if anim_args.hybrid_composite == 'Normal':
|
| 415 |
+
args, prev_img = hybrid_composite(args, anim_args, frame_idx, prev_img, depth_model, hybrid_comp_schedules, root)
|
| 416 |
+
|
| 417 |
+
# apply color matching
|
| 418 |
+
if anim_args.color_coherence != 'None':
|
| 419 |
+
if color_match_sample is None:
|
| 420 |
+
color_match_sample = prev_img.copy()
|
| 421 |
+
else:
|
| 422 |
+
prev_img = maintain_colors(prev_img, color_match_sample, anim_args.color_coherence)
|
| 423 |
+
|
| 424 |
+
# intercept and override to grayscale
|
| 425 |
+
if anim_args.color_force_grayscale:
|
| 426 |
+
prev_img = cv2.cvtColor(prev_img, cv2.COLOR_BGR2GRAY)
|
| 427 |
+
prev_img = cv2.cvtColor(prev_img, cv2.COLOR_GRAY2BGR)
|
| 428 |
+
|
| 429 |
+
# apply scaling
|
| 430 |
+
contrast_image = (prev_img * contrast).round().astype(np.uint8)
|
| 431 |
+
# anti-blur
|
| 432 |
+
if amount > 0:
|
| 433 |
+
contrast_image = unsharp_mask(contrast_image, (kernel, kernel), sigma, amount, threshold, mask_image if args.use_mask else None)
|
| 434 |
+
# apply frame noising
|
| 435 |
+
if args.use_mask or anim_args.use_noise_mask:
|
| 436 |
+
root.noise_mask = compose_mask_with_check(root, args, noise_mask_seq, noise_mask_vals, Image.fromarray(cv2.cvtColor(contrast_image, cv2.COLOR_BGR2RGB)))
|
| 437 |
+
noised_image = add_noise(contrast_image, noise, args.seed, anim_args.noise_type,
|
| 438 |
+
(anim_args.perlin_w, anim_args.perlin_h, anim_args.perlin_octaves, anim_args.perlin_persistence),
|
| 439 |
+
root.noise_mask, args.invert_mask)
|
| 440 |
+
|
| 441 |
+
# use transformed previous frame as init for current
|
| 442 |
+
args.use_init = True
|
| 443 |
+
root.init_sample = Image.fromarray(cv2.cvtColor(noised_image, cv2.COLOR_BGR2RGB))
|
| 444 |
+
args.strength = max(0.0, min(1.0, strength))
|
| 445 |
+
|
| 446 |
+
args.scale = scale
|
| 447 |
+
|
| 448 |
+
# Pix2Pix Image CFG Scale - does *nothing* with non pix2pix checkpoints
|
| 449 |
+
args.pix2pix_img_cfg_scale = float(keys.pix2pix_img_cfg_scale_series[frame_idx])
|
| 450 |
+
|
| 451 |
+
# grab prompt for current frame
|
| 452 |
+
args.prompt = prompt_series[frame_idx]
|
| 453 |
+
|
| 454 |
+
if args.seed_behavior == 'schedule' or parseq_adapter.manages_seed():
|
| 455 |
+
args.seed = int(keys.seed_schedule_series[frame_idx])
|
| 456 |
+
|
| 457 |
+
if anim_args.enable_checkpoint_scheduling:
|
| 458 |
+
args.checkpoint = keys.checkpoint_schedule_series[frame_idx]
|
| 459 |
+
else:
|
| 460 |
+
args.checkpoint = None
|
| 461 |
+
|
| 462 |
+
# SubSeed scheduling
|
| 463 |
+
if anim_args.enable_subseed_scheduling:
|
| 464 |
+
root.subseed = int(keys.subseed_schedule_series[frame_idx])
|
| 465 |
+
root.subseed_strength = float(keys.subseed_strength_schedule_series[frame_idx])
|
| 466 |
+
|
| 467 |
+
if parseq_adapter.manages_seed():
|
| 468 |
+
anim_args.enable_subseed_scheduling = True
|
| 469 |
+
root.subseed = int(keys.subseed_schedule_series[frame_idx])
|
| 470 |
+
root.subseed_strength = keys.subseed_strength_schedule_series[frame_idx]
|
| 471 |
+
|
| 472 |
+
# set value back into the prompt - prepare and report prompt and seed
|
| 473 |
+
args.prompt = prepare_prompt(args.prompt, anim_args.max_frames, args.seed, frame_idx)
|
| 474 |
+
|
| 475 |
+
# grab init image for current frame
|
| 476 |
+
if using_vid_init:
|
| 477 |
+
init_frame = get_next_frame(args.outdir, anim_args.video_init_path, frame_idx, False)
|
| 478 |
+
print(f"Using video init frame {init_frame}")
|
| 479 |
+
args.init_image = init_frame
|
| 480 |
+
args.strength = max(0.0, min(1.0, strength))
|
| 481 |
+
if anim_args.use_mask_video:
|
| 482 |
+
args.mask_file = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 483 |
+
root.noise_mask = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 484 |
+
|
| 485 |
+
mask_vals['video_mask'] = get_mask_from_file(get_next_frame(args.outdir, anim_args.video_mask_path, frame_idx, True), args)
|
| 486 |
+
|
| 487 |
+
if args.use_mask:
|
| 488 |
+
args.mask_image = compose_mask_with_check(root, args, mask_seq, mask_vals, root.init_sample) if root.init_sample is not None else None # we need it only after the first frame anyway
|
| 489 |
+
|
| 490 |
+
# setting up some arguments for the looper
|
| 491 |
+
loop_args.imageStrength = loopSchedulesAndData.image_strength_schedule_series[frame_idx]
|
| 492 |
+
loop_args.blendFactorMax = loopSchedulesAndData.blendFactorMax_series[frame_idx]
|
| 493 |
+
loop_args.blendFactorSlope = loopSchedulesAndData.blendFactorSlope_series[frame_idx]
|
| 494 |
+
loop_args.tweeningFrameSchedule = loopSchedulesAndData.tweening_frames_schedule_series[frame_idx]
|
| 495 |
+
loop_args.colorCorrectionFactor = loopSchedulesAndData.color_correction_factor_series[frame_idx]
|
| 496 |
+
loop_args.use_looper = loopSchedulesAndData.use_looper
|
| 497 |
+
loop_args.imagesToKeyframe = loopSchedulesAndData.imagesToKeyframe
|
| 498 |
+
|
| 499 |
+
if 'img2img_fix_steps' in opts.data and opts.data["img2img_fix_steps"]: # disable "with img2img do exactly x steps" from general setting, as it *ruins* deforum animations
|
| 500 |
+
opts.data["img2img_fix_steps"] = False
|
| 501 |
+
if scheduled_clipskip is not None:
|
| 502 |
+
opts.data["CLIP_stop_at_last_layers"] = scheduled_clipskip
|
| 503 |
+
if scheduled_noise_multiplier is not None:
|
| 504 |
+
opts.data["initial_noise_multiplier"] = scheduled_noise_multiplier
|
| 505 |
+
if scheduled_ddim_eta is not None:
|
| 506 |
+
opts.data["eta_ddim"] = scheduled_ddim_eta
|
| 507 |
+
if scheduled_ancestral_eta is not None:
|
| 508 |
+
opts.data["eta_ancestral"] = scheduled_ancestral_eta
|
| 509 |
+
|
| 510 |
+
if anim_args.animation_mode == '3D' and (cmd_opts.lowvram or cmd_opts.medvram):
|
| 511 |
+
if predict_depths: depth_model.to('cpu')
|
| 512 |
+
devices.torch_gc()
|
| 513 |
+
lowvram.setup_for_low_vram(sd_model, cmd_opts.medvram)
|
| 514 |
+
sd_hijack.model_hijack.hijack(sd_model)
|
| 515 |
+
|
| 516 |
+
# optical flow redo before generation
|
| 517 |
+
if anim_args.optical_flow_redo_generation != 'None' and prev_img is not None and strength > 0:
|
| 518 |
+
print(f"Optical flow redo is diffusing and warping using {anim_args.optical_flow_redo_generation} optical flow before generation.")
|
| 519 |
+
stored_seed = args.seed
|
| 520 |
+
args.seed = random.randint(0, 2 ** 32 - 1)
|
| 521 |
+
disposable_image = generate(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame_idx, sampler_name=scheduled_sampler_name)
|
| 522 |
+
disposable_image = cv2.cvtColor(np.array(disposable_image), cv2.COLOR_RGB2BGR)
|
| 523 |
+
disposable_flow = get_flow_from_images(prev_img, disposable_image, anim_args.optical_flow_redo_generation, raft_model)
|
| 524 |
+
disposable_image = cv2.cvtColor(disposable_image, cv2.COLOR_BGR2RGB)
|
| 525 |
+
disposable_image = image_transform_optical_flow(disposable_image, disposable_flow, redo_flow_factor)
|
| 526 |
+
args.seed = stored_seed
|
| 527 |
+
root.init_sample = Image.fromarray(disposable_image)
|
| 528 |
+
del (disposable_image, disposable_flow, stored_seed)
|
| 529 |
+
gc.collect()
|
| 530 |
+
|
| 531 |
+
# diffusion redo
|
| 532 |
+
if int(anim_args.diffusion_redo) > 0 and prev_img is not None and strength > 0:
|
| 533 |
+
stored_seed = args.seed
|
| 534 |
+
for n in range(0, int(anim_args.diffusion_redo)):
|
| 535 |
+
print(f"Redo generation {n + 1} of {int(anim_args.diffusion_redo)} before final generation")
|
| 536 |
+
args.seed = random.randint(0, 2 ** 32 - 1)
|
| 537 |
+
disposable_image = generate(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame_idx, sampler_name=scheduled_sampler_name)
|
| 538 |
+
disposable_image = cv2.cvtColor(np.array(disposable_image), cv2.COLOR_RGB2BGR)
|
| 539 |
+
# color match on last one only
|
| 540 |
+
if n == int(anim_args.diffusion_redo):
|
| 541 |
+
disposable_image = maintain_colors(prev_img, color_match_sample, anim_args.color_coherence)
|
| 542 |
+
args.seed = stored_seed
|
| 543 |
+
root.init_sample = Image.fromarray(cv2.cvtColor(disposable_image, cv2.COLOR_BGR2RGB))
|
| 544 |
+
del (disposable_image, stored_seed)
|
| 545 |
+
gc.collect()
|
| 546 |
+
|
| 547 |
+
# generation
|
| 548 |
+
image = generate(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame_idx, sampler_name=scheduled_sampler_name)
|
| 549 |
+
|
| 550 |
+
if image is None:
|
| 551 |
+
break
|
| 552 |
+
|
| 553 |
+
# do hybrid video after generation
|
| 554 |
+
if frame_idx > 0 and anim_args.hybrid_composite == 'After Generation':
|
| 555 |
+
image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
| 556 |
+
args, image = hybrid_composite(args, anim_args, frame_idx, image, depth_model, hybrid_comp_schedules, root)
|
| 557 |
+
image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
| 558 |
+
|
| 559 |
+
# color matching on first frame is after generation, color match was collected earlier, so we do an extra generation to avoid the corruption introduced by the color match of first output
|
| 560 |
+
if frame_idx == 0 and (anim_args.color_coherence == 'Image' or (anim_args.color_coherence == 'Video Input' and hybrid_available)):
|
| 561 |
+
image = maintain_colors(cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR), color_match_sample, anim_args.color_coherence)
|
| 562 |
+
image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
| 563 |
+
elif color_match_sample is not None and anim_args.color_coherence != 'None' and not anim_args.legacy_colormatch:
|
| 564 |
+
image = maintain_colors(cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR), color_match_sample, anim_args.color_coherence)
|
| 565 |
+
image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
| 566 |
+
|
| 567 |
+
# intercept and override to grayscale
|
| 568 |
+
if anim_args.color_force_grayscale:
|
| 569 |
+
image = ImageOps.grayscale(image)
|
| 570 |
+
image = ImageOps.colorize(image, black="black", white="white")
|
| 571 |
+
|
| 572 |
+
# overlay mask
|
| 573 |
+
if args.overlay_mask and (anim_args.use_mask_video or args.use_mask):
|
| 574 |
+
image = do_overlay_mask(args, anim_args, image, frame_idx)
|
| 575 |
+
|
| 576 |
+
# on strength 0, set color match to generation
|
| 577 |
+
if ((not anim_args.legacy_colormatch and not args.use_init) or (anim_args.legacy_colormatch and strength == 0)) and not anim_args.color_coherence in ['Image', 'Video Input']:
|
| 578 |
+
color_match_sample = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
|
| 579 |
+
|
| 580 |
+
opencv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
| 581 |
+
if not using_vid_init:
|
| 582 |
+
prev_img = opencv_image
|
| 583 |
+
|
| 584 |
+
if turbo_steps > 1:
|
| 585 |
+
turbo_prev_image, turbo_prev_frame_idx = turbo_next_image, turbo_next_frame_idx
|
| 586 |
+
turbo_next_image, turbo_next_frame_idx = opencv_image, frame_idx
|
| 587 |
+
frame_idx += turbo_steps
|
| 588 |
+
else:
|
| 589 |
+
filename = f"{root.timestring}_{frame_idx:09}.png"
|
| 590 |
+
save_image(image, 'PIL', filename, args, video_args, root)
|
| 591 |
+
|
| 592 |
+
if anim_args.save_depth_maps:
|
| 593 |
+
if cmd_opts.lowvram or cmd_opts.medvram:
|
| 594 |
+
lowvram.send_everything_to_cpu()
|
| 595 |
+
sd_hijack.model_hijack.undo_hijack(sd_model)
|
| 596 |
+
devices.torch_gc()
|
| 597 |
+
depth_model.to(root.device)
|
| 598 |
+
depth = depth_model.predict(opencv_image, anim_args.midas_weight, root.half_precision)
|
| 599 |
+
depth_model.save(os.path.join(args.outdir, f"{root.timestring}_depth_{frame_idx:09}.png"), depth)
|
| 600 |
+
if cmd_opts.lowvram or cmd_opts.medvram:
|
| 601 |
+
depth_model.to('cpu')
|
| 602 |
+
devices.torch_gc()
|
| 603 |
+
lowvram.setup_for_low_vram(sd_model, cmd_opts.medvram)
|
| 604 |
+
sd_hijack.model_hijack.hijack(sd_model)
|
| 605 |
+
frame_idx += 1
|
| 606 |
+
|
| 607 |
+
state.current_image = image
|
| 608 |
+
|
| 609 |
+
args.seed = next_seed(args, root)
|
| 610 |
+
|
| 611 |
+
if predict_depths and not keep_in_vram:
|
| 612 |
+
depth_model.delete_model() # handles adabins too
|
| 613 |
+
|
| 614 |
+
if load_raft:
|
| 615 |
+
raft_model.delete_model()
|
scripts/deforum_helpers/render_modes.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import pathlib
|
| 4 |
+
import re
|
| 5 |
+
import numexpr
|
| 6 |
+
from modules.shared import opts, state
|
| 7 |
+
from .render import render_animation
|
| 8 |
+
from .seed import next_seed
|
| 9 |
+
from .video_audio_utilities import vid2frames
|
| 10 |
+
from .prompt import interpolate_prompts
|
| 11 |
+
from .generate import generate
|
| 12 |
+
from .animation_key_frames import DeformAnimKeys
|
| 13 |
+
from .parseq_adapter import ParseqAdapter
|
| 14 |
+
from .save_images import save_image
|
| 15 |
+
from .settings import save_settings_from_animation_run
|
| 16 |
+
|
| 17 |
+
def render_input_video(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root):
|
| 18 |
+
# create a folder for the video input frames to live in
|
| 19 |
+
video_in_frame_path = os.path.join(args.outdir, 'inputframes')
|
| 20 |
+
os.makedirs(video_in_frame_path, exist_ok=True)
|
| 21 |
+
|
| 22 |
+
# save the video frames from input video
|
| 23 |
+
print(f"Exporting Video Frames (1 every {anim_args.extract_nth_frame}) frames to {video_in_frame_path}...")
|
| 24 |
+
vid2frames(video_path = anim_args.video_init_path, video_in_frame_path=video_in_frame_path, n=anim_args.extract_nth_frame, overwrite=anim_args.overwrite_extracted_frames, extract_from_frame=anim_args.extract_from_frame, extract_to_frame=anim_args.extract_to_frame)
|
| 25 |
+
|
| 26 |
+
# determine max frames from length of input frames
|
| 27 |
+
anim_args.max_frames = len([f for f in pathlib.Path(video_in_frame_path).glob('*.jpg')])
|
| 28 |
+
args.use_init = True
|
| 29 |
+
print(f"Loading {anim_args.max_frames} input frames from {video_in_frame_path} and saving video frames to {args.outdir}")
|
| 30 |
+
|
| 31 |
+
if anim_args.use_mask_video:
|
| 32 |
+
# create a folder for the mask video input frames to live in
|
| 33 |
+
mask_in_frame_path = os.path.join(args.outdir, 'maskframes')
|
| 34 |
+
os.makedirs(mask_in_frame_path, exist_ok=True)
|
| 35 |
+
|
| 36 |
+
# save the video frames from mask video
|
| 37 |
+
print(f"Exporting Video Frames (1 every {anim_args.extract_nth_frame}) frames to {mask_in_frame_path}...")
|
| 38 |
+
vid2frames(video_path=anim_args.video_mask_path,video_in_frame_path=mask_in_frame_path, n=anim_args.extract_nth_frame, overwrite=anim_args.overwrite_extracted_frames, extract_from_frame=anim_args.extract_from_frame, extract_to_frame=anim_args.extract_to_frame)
|
| 39 |
+
max_mask_frames = len([f for f in pathlib.Path(mask_in_frame_path).glob('*.jpg')])
|
| 40 |
+
|
| 41 |
+
# limit max frames if there are less frames in the video mask compared to input video
|
| 42 |
+
if max_mask_frames < anim_args.max_frames :
|
| 43 |
+
anim_args.max_mask_frames
|
| 44 |
+
print ("Video mask contains less frames than init video, max frames limited to number of mask frames.")
|
| 45 |
+
args.use_mask = True
|
| 46 |
+
args.overlay_mask = True
|
| 47 |
+
|
| 48 |
+
render_animation(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root)
|
| 49 |
+
|
| 50 |
+
# Modified a copy of the above to allow using masking video with out a init video.
|
| 51 |
+
def render_animation_with_video_mask(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root):
|
| 52 |
+
# create a folder for the video input frames to live in
|
| 53 |
+
mask_in_frame_path = os.path.join(args.outdir, 'maskframes')
|
| 54 |
+
os.makedirs(mask_in_frame_path, exist_ok=True)
|
| 55 |
+
|
| 56 |
+
# save the video frames from mask video
|
| 57 |
+
print(f"Exporting Video Frames (1 every {anim_args.extract_nth_frame}) frames to {mask_in_frame_path}...")
|
| 58 |
+
vid2frames(video_path=anim_args.video_mask_path, video_in_frame_path=mask_in_frame_path, n=anim_args.extract_nth_frame, overwrite=anim_args.overwrite_extracted_frames, extract_from_frame=anim_args.extract_from_frame, extract_to_frame=anim_args.extract_to_frame)
|
| 59 |
+
args.use_mask = True
|
| 60 |
+
#args.overlay_mask = True
|
| 61 |
+
|
| 62 |
+
# determine max frames from length of input frames
|
| 63 |
+
anim_args.max_frames = len([f for f in pathlib.Path(mask_in_frame_path).glob('*.jpg')])
|
| 64 |
+
#args.use_init = True
|
| 65 |
+
print(f"Loading {anim_args.max_frames} input frames from {mask_in_frame_path} and saving video frames to {args.outdir}")
|
| 66 |
+
|
| 67 |
+
render_animation(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root)
|
| 68 |
+
|
| 69 |
+
def get_parsed_value(value, frame_idx, max_f):
|
| 70 |
+
pattern = r'`.*?`'
|
| 71 |
+
regex = re.compile(pattern)
|
| 72 |
+
parsed_value = value
|
| 73 |
+
for match in regex.finditer(parsed_value):
|
| 74 |
+
matched_string = match.group(0)
|
| 75 |
+
parsed_string = matched_string.replace('t', f'{frame_idx}').replace("max_f" , f"{max_f}").replace('`','')
|
| 76 |
+
value = numexpr.evaluate(parsed_string)
|
| 77 |
+
parsed_value = parsed_value.replace(matched_string, str(value))
|
| 78 |
+
return parsed_value
|
| 79 |
+
|
| 80 |
+
def render_interpolation(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root):
|
| 81 |
+
|
| 82 |
+
# use parseq if manifest is provided
|
| 83 |
+
parseq_adapter = ParseqAdapter(parseq_args, anim_args, video_args, controlnet_args)
|
| 84 |
+
|
| 85 |
+
# expand key frame strings to values
|
| 86 |
+
keys = DeformAnimKeys(anim_args) if not parseq_adapter.use_parseq else parseq_adapter.anim_keys
|
| 87 |
+
|
| 88 |
+
# create output folder for the batch
|
| 89 |
+
os.makedirs(args.outdir, exist_ok=True)
|
| 90 |
+
print(f"Saving interpolation animation frames to {args.outdir}")
|
| 91 |
+
|
| 92 |
+
# save settings.txt file for the current run
|
| 93 |
+
save_settings_from_animation_run(args, anim_args, parseq_args, loop_args, controlnet_args, video_args, root)
|
| 94 |
+
|
| 95 |
+
# Compute interpolated prompts
|
| 96 |
+
if parseq_adapter.manages_prompts():
|
| 97 |
+
print("Parseq prompts are assumed to already be interpolated - not doing any additional prompt interpolation")
|
| 98 |
+
prompt_series = keys.prompts
|
| 99 |
+
else:
|
| 100 |
+
print("Generating interpolated prompts for all frames")
|
| 101 |
+
prompt_series = interpolate_prompts(root.animation_prompts, anim_args.max_frames)
|
| 102 |
+
|
| 103 |
+
state.job_count = anim_args.max_frames
|
| 104 |
+
frame_idx = 0
|
| 105 |
+
# INTERPOLATION MODE
|
| 106 |
+
while frame_idx < anim_args.max_frames:
|
| 107 |
+
# print data to cli
|
| 108 |
+
prompt_to_print = get_parsed_value(prompt_series[frame_idx].strip(), frame_idx, anim_args.max_frames)
|
| 109 |
+
|
| 110 |
+
if prompt_to_print.endswith("--neg"):
|
| 111 |
+
prompt_to_print = prompt_to_print[:-5]
|
| 112 |
+
print(f"\033[36mInterpolation frame: \033[0m{frame_idx}/{anim_args.max_frames} ")
|
| 113 |
+
print(f"\033[32mSeed: \033[0m{args.seed}")
|
| 114 |
+
print(f"\033[35mPrompt: \033[0m{prompt_to_print}")
|
| 115 |
+
|
| 116 |
+
state.job = f"frame {frame_idx + 1}/{anim_args.max_frames}"
|
| 117 |
+
state.job_no = frame_idx + 1
|
| 118 |
+
|
| 119 |
+
if state.interrupted:
|
| 120 |
+
break
|
| 121 |
+
if state.skipped:
|
| 122 |
+
print("\n** PAUSED **")
|
| 123 |
+
state.skipped = False
|
| 124 |
+
while not state.skipped:
|
| 125 |
+
time.sleep(0.1)
|
| 126 |
+
print("** RESUMING **")
|
| 127 |
+
|
| 128 |
+
# grab inputs for current frame generation
|
| 129 |
+
args.prompt = prompt_to_print
|
| 130 |
+
args.scale = keys.cfg_scale_schedule_series[frame_idx]
|
| 131 |
+
args.pix2pix_img_cfg_scale = keys.pix2pix_img_cfg_scale_series[frame_idx]
|
| 132 |
+
|
| 133 |
+
scheduled_sampler_name = keys.sampler_schedule_series[frame_idx].casefold() if anim_args.enable_sampler_scheduling and keys.sampler_schedule_series[frame_idx] is not None else None
|
| 134 |
+
args.steps = int(keys.steps_schedule_series[frame_idx]) if anim_args.enable_steps_scheduling and keys.steps_schedule_series[frame_idx] is not None else args.steps
|
| 135 |
+
scheduled_clipskip = int(keys.clipskip_schedule_series[frame_idx]) if anim_args.enable_clipskip_scheduling and keys.clipskip_schedule_series[frame_idx] is not None else None
|
| 136 |
+
args.checkpoint = keys.checkpoint_schedule_series[frame_idx] if anim_args.enable_checkpoint_scheduling else None
|
| 137 |
+
if anim_args.enable_subseed_scheduling:
|
| 138 |
+
root.subseed = int(keys.subseed_schedule_series[frame_idx])
|
| 139 |
+
root.subseed_strength = keys.subseed_strength_schedule_series[frame_idx]
|
| 140 |
+
else:
|
| 141 |
+
root.subseed, root.subseed_strength = keys.subseed_schedule_series[frame_idx], keys.subseed_strength_schedule_series[frame_idx]
|
| 142 |
+
if parseq_adapter.manages_seed():
|
| 143 |
+
anim_args.enable_subseed_scheduling = True
|
| 144 |
+
root.subseed, root.subseed_strength = int(keys.subseed_schedule_series[frame_idx]), keys.subseed_strength_schedule_series[frame_idx]
|
| 145 |
+
args.seed = int(keys.seed_schedule_series[frame_idx]) if (args.seed_behavior == 'schedule' or parseq_adapter.manages_seed()) else args.seed
|
| 146 |
+
opts.data["CLIP_stop_at_last_layers"] = scheduled_clipskip if scheduled_clipskip is not None else opts.data["CLIP_stop_at_last_layers"]
|
| 147 |
+
|
| 148 |
+
image = generate(args, keys, anim_args, loop_args, controlnet_args, root, parseq_adapter, frame_idx, sampler_name=scheduled_sampler_name)
|
| 149 |
+
filename = f"{root.timestring}_{frame_idx:09}.png"
|
| 150 |
+
|
| 151 |
+
save_image(image, 'PIL', filename, args, video_args, root)
|
| 152 |
+
|
| 153 |
+
state.current_image = image
|
| 154 |
+
|
| 155 |
+
if args.seed_behavior != 'schedule':
|
| 156 |
+
args.seed = next_seed(args, root)
|
| 157 |
+
|
| 158 |
+
frame_idx += 1
|
scripts/deforum_helpers/resume.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
from modules.shared import opts
|
| 4 |
+
|
| 5 |
+
# Resume requires at least two actual frames in order to work
|
| 6 |
+
# 'Actual' frames are defined as frames that go through generation
|
| 7 |
+
# - Can't resume from a single frame.
|
| 8 |
+
# - If you have a cadence of 10, you need at least 10 frames in order to resume.
|
| 9 |
+
# - Resume grabs the last actual frame and the 2nd to last actual frame
|
| 10 |
+
# in order to work with cadence properly and feed it the prev_img/next_img
|
| 11 |
+
|
| 12 |
+
def get_resume_vars(folder, timestring, cadence):
|
| 13 |
+
DEBUG_MODE = opts.data.get("deforum_debug_mode_enabled", False)
|
| 14 |
+
# count previous frames
|
| 15 |
+
frame_count = 0
|
| 16 |
+
for item in os.listdir(folder):
|
| 17 |
+
# don't count txt files or mp4 files
|
| 18 |
+
if ".txt" in item or ".mp4" in item:
|
| 19 |
+
pass
|
| 20 |
+
else:
|
| 21 |
+
filename = item.split("_")
|
| 22 |
+
# other image file types may be supported in the future,
|
| 23 |
+
# so we just count files containing timestring
|
| 24 |
+
# that don't contain the depth keyword (depth maps are saved in same folder)
|
| 25 |
+
if timestring in filename and "depth" not in filename:
|
| 26 |
+
frame_count += 1
|
| 27 |
+
# add this to debugging var
|
| 28 |
+
if DEBUG_MODE:
|
| 29 |
+
print(f"\033[36mResuming:\033[0m File: {filename}")
|
| 30 |
+
|
| 31 |
+
print(f"\033[36mResuming:\033[0m Current frame count: {frame_count}")
|
| 32 |
+
|
| 33 |
+
# get last frame from frame count corrected for any trailing cadence frames
|
| 34 |
+
last_frame = frame_count - (frame_count % cadence)
|
| 35 |
+
|
| 36 |
+
# calculate previous actual frame
|
| 37 |
+
prev_frame = last_frame - cadence
|
| 38 |
+
|
| 39 |
+
# calculate next actual frame
|
| 40 |
+
next_frame = last_frame - 1
|
| 41 |
+
|
| 42 |
+
# get prev_img/next_img from prev/next frame index (files start at 0, so subtract 1 for index var)
|
| 43 |
+
path = os.path.join(folder, f"{timestring}_{prev_frame:09}.png")
|
| 44 |
+
prev_img = cv2.imread(path)
|
| 45 |
+
path = os.path.join(folder, f"{timestring}_{next_frame:09}.png")
|
| 46 |
+
next_img = cv2.imread(path)
|
| 47 |
+
|
| 48 |
+
# report resume last/next in console
|
| 49 |
+
print(f"\033[36mResuming:\033[0m Last frame: {prev_frame} - Next frame: {next_frame} ")
|
| 50 |
+
|
| 51 |
+
# returns:
|
| 52 |
+
# last frame count, accounting for cadence
|
| 53 |
+
# next frame count, accounting for cadence
|
| 54 |
+
# prev frame's image cv2 BGR
|
| 55 |
+
# next frame's image cv2 BGR
|
| 56 |
+
return prev_frame, next_frame, prev_img, next_img
|
scripts/deforum_helpers/rich.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rich.console import Console
|
| 2 |
+
console = Console()
|