ddoc commited on
Commit
4182270
·
1 Parent(s): c3ea622

Upload 170 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  2. .github/ISSUE_TEMPLATE/config.yml +8 -0
  3. .github/ISSUE_TEMPLATE/feature_request.yml +46 -0
  4. .github/scripts/issue_checker.py +110 -0
  5. .github/workflows/issue_checker.yaml +23 -0
  6. .gitignore +12 -0
  7. CONTRIBUTING.md +3 -0
  8. LICENSE +0 -0
  9. README.md +67 -0
  10. install.py +10 -0
  11. javascript/deforum-hints.js +214 -0
  12. javascript/deforum.js +15 -0
  13. requirements.txt +8 -0
  14. scripts/deforum.py +22 -0
  15. scripts/deforum_extend_paths.py +17 -0
  16. scripts/deforum_helpers/RAFT.py +28 -0
  17. scripts/deforum_helpers/animation.py +413 -0
  18. scripts/deforum_helpers/animation_key_frames.py +133 -0
  19. scripts/deforum_helpers/args.py +1134 -0
  20. scripts/deforum_helpers/auto_navigation.py +72 -0
  21. scripts/deforum_helpers/colors.py +22 -0
  22. scripts/deforum_helpers/composable_masks.py +196 -0
  23. scripts/deforum_helpers/consistency_check.py +132 -0
  24. scripts/deforum_helpers/defaults.py +203 -0
  25. scripts/deforum_helpers/deforum_controlnet.py +324 -0
  26. scripts/deforum_helpers/deforum_controlnet_gradio.py +34 -0
  27. scripts/deforum_helpers/deforum_tqdm.py +82 -0
  28. scripts/deforum_helpers/deprecation_utils.py +82 -0
  29. scripts/deforum_helpers/depth.py +143 -0
  30. scripts/deforum_helpers/depth_adabins.py +62 -0
  31. scripts/deforum_helpers/depth_leres.py +55 -0
  32. scripts/deforum_helpers/depth_midas.py +75 -0
  33. scripts/deforum_helpers/depth_zoe.py +30 -0
  34. scripts/deforum_helpers/frame_interpolation.py +224 -0
  35. scripts/deforum_helpers/general_utils.py +128 -0
  36. scripts/deforum_helpers/generate.py +310 -0
  37. scripts/deforum_helpers/gradio_funcs.py +280 -0
  38. scripts/deforum_helpers/human_masking.py +70 -0
  39. scripts/deforum_helpers/hybrid_video.py +594 -0
  40. scripts/deforum_helpers/image_sharpening.py +22 -0
  41. scripts/deforum_helpers/load_images.py +96 -0
  42. scripts/deforum_helpers/masks.py +40 -0
  43. scripts/deforum_helpers/noise.py +72 -0
  44. scripts/deforum_helpers/parseq_adapter.py +243 -0
  45. scripts/deforum_helpers/parseq_adapter_test.py +227 -0
  46. scripts/deforum_helpers/prompt.py +140 -0
  47. scripts/deforum_helpers/render.py +615 -0
  48. scripts/deforum_helpers/render_modes.py +158 -0
  49. scripts/deforum_helpers/resume.py +56 -0
  50. 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
+ ![adsdasunknown](https://user-images.githubusercontent.com/14872007/196064311-1b79866a-e55b-438a-84a7-004ff30829ad.png)
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
+ ![image](https://user-images.githubusercontent.com/121192995/226101131-43bf594a-3152-45dd-a5d1-2538d0bc221d.png)
64
+
65
+ Keyframes tab:
66
+
67
+ ![image](https://user-images.githubusercontent.com/121192995/226101140-bfe6cce7-9b78-4a1d-be9a-43e1fc78239e.png)
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 &lt;&gt;, like &lt;apple&gt;, &lt;hair&gt</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()