File size: 17,184 Bytes
b09e573
 
 
 
 
 
 
 
c914404
66c8ea4
b09e573
 
 
66c8ea4
c914404
cc5a426
c914404
b09e573
 
 
 
c914404
 
 
 
 
 
 
b09e573
 
 
 
 
 
c914404
b09e573
 
c914404
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
b09e573
 
 
 
 
 
 
 
 
c914404
b09e573
 
 
 
 
c914404
b09e573
 
c914404
b09e573
 
 
 
 
 
 
 
 
 
 
c914404
 
 
 
 
 
 
b09e573
c914404
 
 
b09e573
c914404
 
 
b09e573
c914404
 
 
b09e573
c914404
 
b09e573
c914404
b09e573
c914404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc5a426
c914404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
 
 
5a7f927
1d3bf88
c914404
 
 
 
 
1d3bf88
c914404
5a7f927
cc5a426
5a7f927
 
 
 
 
 
c914404
cc5a426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
 
 
 
 
 
c914404
1d3bf88
 
 
5a7f927
 
 
 
c914404
 
 
1d3bf88
c914404
 
 
1d3bf88
 
 
 
 
 
c914404
1d3bf88
c914404
 
 
 
 
 
1d3bf88
 
 
c914404
 
 
b09e573
 
 
 
 
c914404
1d3bf88
c914404
1d3bf88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b09e573
1d3bf88
 
 
 
 
 
 
 
 
 
 
 
 
 
b09e573
c914404
b09e573
c914404
 
 
263de18
 
c914404
 
 
 
 
 
1d3bf88
 
 
 
 
 
 
cc5a426
 
 
1d3bf88
c914404
 
 
 
 
 
 
 
 
5a7f927
c914404
1d3bf88
c914404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
c914404
 
 
 
 
 
 
 
5a7f927
c914404
5a7f927
c914404
 
 
 
1d3bf88
 
 
5a7f927
1d3bf88
5a7f927
1d3bf88
 
 
 
 
 
5a7f927
1d3bf88
5a7f927
1d3bf88
 
 
 
 
 
 
 
5a7f927
1d3bf88
 
 
 
cc5a426
 
 
 
 
 
 
 
 
b09e573
 
c914404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf88
 
cc5a426
1d3bf88
c914404
 
 
 
 
cc5a426
c914404
 
 
263de18
c914404
 
 
 
 
 
 
1d3bf88
 
 
cc5a426
 
c914404
 
 
b09e573
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
from spandrel import ModelLoader
import torch
from pathlib import Path
import gradio as App
import logging
import spaces
import time
import cv2
import os

from gradio import themes
from rich.console import Console
from rich.logging import RichHandler

from Scripts.SAD import GetDifferenceRectangles
from Scripts.ORB import DetectMotionWithOrb

# ============================== #
#          Core Settings         #
# ============================== #

Theme = themes.Citrus(
	primary_hue='blue',
	secondary_hue='blue',
	radius_size=themes.sizes.radius_xxl
).set(
	link_text_color='blue'
)
ModelDir = Path('./Models')
TempDir = Path('./Temp')
os.environ['GRADIO_TEMP_DIR'] = str(TempDir)
ModelFileType = '.pth'

# ============================== #
#            Logging             #
# ============================== #

logging.basicConfig(
	level=logging.INFO,
	format='%(message)s',
	datefmt='[%X]',
	handlers=[RichHandler(
		console=Console(),
		rich_tracebacks=True,
		omit_repeated_times=False,
		markup=True,
		show_path=False,
	)],
)
Logger = logging.getLogger('Zero2x')
logging.getLogger('httpx').setLevel(logging.WARNING)

# ============================== #
#      Device Configuration      #
# ============================== #

@spaces.GPU
def GetDeviceName():
	Device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
	Logger.info(f'πŸ§ͺ Using device: {str(Device).upper()}')
	return Device

Device = GetDeviceName()

# ============================== #
#       Utility Functions        #
# ============================== #

def HumanizeSeconds(Seconds):
	Hours = int(Seconds // 3600)
	Minutes = int((Seconds % 3600) // 60)
	Seconds = int(Seconds % 60)

	if Hours > 0:
		return f'{Hours}h {Minutes}m {Seconds}s'
	elif Minutes > 0:
		return f'{Minutes}m {Seconds}s'
	else:
		return f'{Seconds}s'

def HumanizedBytes(Size):
	Units = ['B', 'KB', 'MB', 'GB', 'TB']
	Index = 0
	while Size >= 1024 and Index < len(Units) - 1:
		Size /= 1024.0
		Index += 1
	return f'{Size:.2f} {Units[Index]}'

# ============================== #
#     Main Processing Logic      #
# ============================== #

class Upscaler:
	def __init__(self):
		pass

	def ListModels(self):
		Models = sorted(
			[File.name for File in ModelDir.glob('*' + ModelFileType) if File.is_file()]
		)
		Logger.info(f'πŸ“š Found {len(Models)} Models In Directory')
		return Models

	def LoadModel(self, ModelName):
		torch.cuda.empty_cache()
		Model = (
			ModelLoader()
			.load_from_file(ModelDir / (ModelName + ModelFileType))
			.to(Device)
			.eval()
		)
		Logger.info(f'πŸ€– Loaded Model {ModelName} Onto {str(Device).upper()}')
		return Model

	def UnloadModel(self):
		if Device.type == 'cuda':
			torch.cuda.empty_cache()
		Logger.info('πŸ€– Model Unloaded Successfully')

	def CleanUp(self):
		self.UnloadModel()
		Logger.info('🧹 Temporary Files Cleaned Up')

	@spaces.GPU
	def UpscaleFullFrame(self, Model, Frame):
		FrameRgb = cv2.cvtColor(Frame, cv2.COLOR_BGR2RGB)
		FrameForTorch = FrameRgb.transpose(2, 0, 1)
		FrameForTorch = torch.from_numpy(FrameForTorch).unsqueeze(0).to(Device).float() / 255.0
		OutputFrame = Model(FrameForTorch)[0].cpu().numpy().transpose(1, 2, 0) * 255.0
		OutputFrame = cv2.cvtColor(OutputFrame.astype('uint8'), cv2.COLOR_RGB2BGR)
		return OutputFrame

	@spaces.GPU
	def UpscaleRegions(self, Model, Frame, PrevFrame, UpscaledPrevFrame, InputThreshold, InputMinPercentage, InputMaxRectangles, InputPadding, InputSegmentRows, InputSegmentColumns):
		DiffResult = GetDifferenceRectangles(
			PrevFrame,
			Frame,
			Threshold=InputThreshold,
			Rows=InputSegmentRows,
			Columns=InputSegmentColumns,
			Padding=InputPadding
		)
		SimilarityPercentage = DiffResult['SimilarPercentage']
		Rectangles = DiffResult['Rectangles']
		Cols = DiffResult['Columns']
		Rows = DiffResult['Rows']
		FrameHeight, FrameWidth = Frame.shape[:2]
		SegmentWidth = FrameWidth // Cols
		SegmentHeight = FrameHeight // Rows
		UseRegions = False
		RegionLog = 'πŸŸ₯'
		if SimilarityPercentage > InputMinPercentage and len(Rectangles) < InputMaxRectangles:
			UpscaleFactorY = UpscaledPrevFrame.shape[0] // FrameHeight
			UpscaleFactorX = UpscaledPrevFrame.shape[1] // FrameWidth
			OutputFrame = UpscaledPrevFrame.copy()
			for X, Y, W, H in Rectangles:
				X1 = X * SegmentWidth
				Y1 = Y * SegmentHeight
				X2 = FrameWidth if X + W == Cols else X1 + W * SegmentWidth
				Y2 = FrameHeight if Y + H == Rows else Y1 + H * SegmentHeight
				Region = Frame[Y1:Y2, X1:X2]
				RegionRgb = cv2.cvtColor(Region, cv2.COLOR_BGR2RGB)
				RegionTorch = torch.from_numpy(RegionRgb.transpose(2, 0, 1)).unsqueeze(0).to(Device).float() / 255.0
				UpscaledRegion = Model(RegionTorch)[0].cpu().numpy().transpose(1, 2, 0) * 255.0
				UpscaledRegion = cv2.cvtColor(UpscaledRegion.astype('uint8'), cv2.COLOR_RGB2BGR)
				RegionHeight, RegionWidth = Region.shape[:2]
				UpscaledRegion = cv2.resize(UpscaledRegion, (RegionWidth * UpscaleFactorX, RegionHeight * UpscaleFactorY), interpolation=cv2.INTER_CUBIC)
				UX1 = X1 * UpscaleFactorX
				UY1 = Y1 * UpscaleFactorY
				UX2 = UX1 + UpscaledRegion.shape[1]
				UY2 = UY1 + UpscaledRegion.shape[0]
				OutputFrame[UY1:UY2, UX1:UX2] = UpscaledRegion
			RegionLog = '🟩'
			UseRegions = True
		else:
			OutputFrame = self.UpscaleFullFrame(Model, Frame)
		return OutputFrame, SimilarityPercentage, Rectangles, RegionLog, UseRegions

	@spaces.GPU
	def Process(self, InputVideo, InputModel, InputUseRegions, InputThreshold, InputMinPercentage, InputMaxRectangles, InputPadding, InputSegmentRows, InputSegmentColumns, InputFullFrameInterval, InputMotionThreshold, Progress=App.Progress()):
		if not InputVideo:
			Logger.warning('❌ No Video Provided')
			App.Warning('❌ No Video Provided')
			return None, None

		Progress(0, desc='βš™οΈ Loading Model')
		Model = self.LoadModel(InputModel)

		Logger.info(f'πŸ“Ό Processing Video: {Path(InputVideo).name}')
		Progress(0, desc='πŸ“Ό Processing Video')
		Video = cv2.VideoCapture(InputVideo)

		FrameRate = Video.get(cv2.CAP_PROP_FPS)
		FrameCount = int(Video.get(cv2.CAP_PROP_FRAME_COUNT))
		Width = int(Video.get(cv2.CAP_PROP_FRAME_WIDTH))
		Height = int(Video.get(cv2.CAP_PROP_FRAME_HEIGHT))

		Logger.info(f'πŸ“ Video Properties: {FrameCount} Frames, {FrameRate} FPS, {Width}x{Height}')

		PerFrameProgress = 1 / FrameCount
		FrameProgress = 0.0
		StartTime = time.time()
		Times = []

		CurrentFrameIndex = 0
		PrevFrame = None
		UpscaledPrevFrame = None
		PartialUpscaleCount = 0

		while True:
			Ret, Frame = Video.read()
			if not Ret:
				break

			CurrentFrameIndex += 1

			ForceFull = False
			CopyPrevUpscaled = False
			if CurrentFrameIndex == 1 or not InputUseRegions:
				ForceFull = True
				PartialUpscaleCount = 0
			elif PartialUpscaleCount >= InputFullFrameInterval:
				ForceFull = True
				PartialUpscaleCount = 0

			if PrevFrame is not None:
				IsMotion, TotalMagnitude, DirectionAngle = DetectMotionWithOrb(PrevFrame, Frame, InputMotionThreshold)
				if IsMotion:
					ForceFull = True
					PartialUpscaleCount = 0
					Logger.info(f'🟨 Frame {CurrentFrameIndex}: Motion Detected - Upscaling Full Frame')

			if not ForceFull and PrevFrame is not None and UpscaledPrevFrame is not None:
				DiffResult = GetDifferenceRectangles(
					PrevFrame,
					Frame,
					Threshold=InputThreshold,
					Rows=InputSegmentRows,
					Columns=InputSegmentColumns,
					Padding=InputPadding
				)
				SimilarityPercentage = DiffResult['SimilarPercentage']
				if SimilarityPercentage == 100:
					OutputFrame = UpscaledPrevFrame.copy()
					RegionLog = '🟦'
					UseRegions = False
					Rectangles = []
					Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: 100% Similar - Copied Previous Upscaled Frame')
					FrameProgress += PerFrameProgress
					Progress(FrameProgress, desc=f'πŸ“¦ Processed Frame {CurrentFrameIndex}/{FrameCount}')
					cv2.imwrite(f'{TempDir}/Upscaled_Frame_{CurrentFrameIndex:05d}.png', OutputFrame)
					PrevFrame = Frame.copy()
					UpscaledPrevFrame = OutputFrame.copy()
					DeltaTime = time.time() - StartTime
					Times.append(DeltaTime)
					StartTime = time.time()
					continue

			if ForceFull:
				OutputFrame = self.UpscaleFullFrame(Model, Frame)
				SimilarityPercentage = 0
				Rectangles = []
				RegionLog = 'πŸŸ₯'
				UseRegions = False
			else:
				OutputFrame, SimilarityPercentage, Rectangles, RegionLog, UseRegions = self.UpscaleRegions(
					Model, Frame, PrevFrame, UpscaledPrevFrame, InputThreshold, InputMinPercentage, InputMaxRectangles, InputPadding, InputSegmentRows, InputSegmentColumns
				)
				if UseRegions:
					PartialUpscaleCount += 1
				else:
					PartialUpscaleCount = 0

			if Times:
				AverageTime = sum(Times) / len(Times)
				Eta = HumanizeSeconds((FrameCount - CurrentFrameIndex) * AverageTime)
			else:
				Eta = None

			if UseRegions:
				Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: {SimilarityPercentage:.2f}% Similar, {len(Rectangles)} Regions To Upscale')
			else:
				Logger.info(f'{RegionLog} Frame {CurrentFrameIndex}: Upscaling Full Frame')

			Progress(FrameProgress, desc=f'πŸ“¦ Processed Frame {CurrentFrameIndex}/{FrameCount} - {Eta}')

			cv2.imwrite(f'{TempDir}/Upscaled_Frame_{CurrentFrameIndex:05d}.png', OutputFrame)

			DeltaTime = time.time() - StartTime
			Times.append(DeltaTime)
			StartTime = time.time()
			FrameProgress += PerFrameProgress

			PrevFrame = Frame.copy()
			UpscaledPrevFrame = OutputFrame.copy()

		Progress(1, desc='πŸ“¦ Cleaning Up')
		self.CleanUp()
		return InputVideo, InputVideo

# ============================== #
#          Streamlined UI        #
# ============================== #

with App.Blocks(
	title='Zero2x Video Upscaler', theme=Theme, delete_cache=(-1, 1800)
) as Interface:
	App.Markdown('# 🎞️ Zero2x Video Upscaler')
	with App.Accordion(label='βš™οΈ About Zero2x', open=False):
		App.Markdown('''
			**Zero2x** is a work-in-progress video upscaling tool that uses deep learning models to enhance your videos frame by frame.  
			This app leverages region-based difference detection to speed up processing and reduce unnecessary computation.

			---

			## ✨ Features

			- **Multiple Upscaling Models:** Choose from a selection of pre-trained models for different styles and quality.
			- **Region-Based Upscaling:** Only upscale parts of the frame that have changed, making processing faster and more memory-efficient.
			- **Full Frame Upscaling:** Optionally upscale every frame in its entirety for maximum quality.
			- **Customizable Settings:** Fine-tune thresholds, padding, and region detection for your specific needs.
			- **Progress Tracking:** See estimated time remaining and per-frame progress.
			- **Downloadable Results:** Download your upscaled video when processing is complete.

			---

			## πŸ§‘β€πŸ”¬ Technique

			This app uses the Segmented Absolute Differences (SAD) (Created by me) program to compare each frame with the previous one.  
			If only small regions have changed, only those regions are upscaled using the selected model.  
			If the whole frame is different, the entire frame is upscaled.  
			This hybrid approach balances speed and quality.

			---

			## 🚧 Work In Progress

			- More models and settings will be added soon.
			- Some features may be experimental or incomplete.
			- Feedback and suggestions are welcome!
			- The quality of the upscaled video may vary depending on the model and settings used.

			---

			**Tip:** If you encounter CUDA out-of-memory errors, try increasing the segment grid size or lowering the region count.
			**Note:** The reason i named this project Zero2x is because i was inspired by Video2x, but i wanted my own version with a different approach.
			It is running on HuggingFace's ZeroGPU hardware, which is why i came up with the name.

			''')
	with App.Row():
		with App.Column():
			with App.Group():
				InputVideo = App.Video(
					label='Input Video', sources=['upload'], height=300
				)
				ModelList = Upscaler().ListModels()
				ModelNames = [Path(Model).stem for Model in ModelList]
				InputModel = App.Dropdown(
					choices=ModelNames,
					label='Select Model',
					value=ModelNames[0],
				)
				with App.Accordion(label='βš™οΈ Advanced Settings', open=False):
					with App.Accordion(label='πŸ“œ Settings Explained', open=False):
						App.Markdown('''
							- **Use Regions:** When enabled, only changed areas between frames are upscaled. This is faster but may miss subtle changes.
							- **Threshold:** Controls how sensitive the difference detection is. I found high values to introduce unmatching regions, be careful.
							- **Padding:** Adds extra pixels around detected regions to include out of bounds pixels.
							- **Min Percentage:** If the similarity between frames is above this value, only regions are upscaled; otherwise, the full frame is upscaled.
							- **Max Rectangles:** Limits the number of regions to process per frame for performance.
							- **Segment Rows/Columns:** Controls the grid size for region detection. More segments allow finer detection but may increase processing time. Uses less Vram when used.
				   			- **Full Frame Interval:** Forces a full-frame upscale every N frames. Set to 1 to always upscale the full frame. This is to prevent regions from glitching out.
				   			- **Motion Threshold:** Controls how sensitive the motion detection is. Upscaling motion frames increases faulty regions. Lower = More strict
						''')
					with App.Group():
						InputUseRegions = App.Checkbox(
							label='Use Regions',
							value=False,
							info='Use regions to upscale only the different parts of the video (⚑️ Experimental, Faster)',
							interactive=True
						)
						InputThreshold = App.Slider(
							label='Threshold',
							value=2,
							minimum=0,
							maximum=10,
							step=0.5,
							info='Threshold for the SAD algorithm to detect different regions',
							interactive=False
						)
						InputPadding = App.Slider(
							label='Padding',
							value=1,
							minimum=0,
							maximum=5,
							step=1,
							info='Extra padding to include neighboring pixels in the SAD algorithm',
							interactive=False
						)
						InputMinPercentage = App.Slider(
							label='Min Percentage',
							value=50,
							minimum=0,
							maximum=100,
							step=1,
							info='Minimum percentage of similarity to consider upscaling the full frame',
							interactive=False
						)
						InputMaxRectangles = App.Slider(
							label='Max Rectangles',
							value=10,
							minimum=1,
							maximum=16,
							step=1,
							info='Maximum number of rectangles to consider upscaling the full frame',
							interactive=False
						)
						with App.Row():
							InputSegmentRows = App.Slider(
								label='Segment Rows',
								value=32,
								minimum=1,
								maximum=64,
								step=1,
								info='Number of rows to segment the video into for processing',
								interactive=False
							)
							InputSegmentColumns = App.Slider(
								label='Segment Columns',
								value=48,
								minimum=1,
								maximum=64,
								step=1,
								info='Number of columns to segment the video into for processing',
								interactive=False
							)
						InputFullFrameInterval = App.Slider(
							label='Full Frame Interval',
							value=5,
							minimum=1,
							maximum=100,
							step=1,
							info='Force a full-frame upscale every N frames (set to 1 to always upscale full frame)',
							interactive=False
						)
						InputMotionThreshold = App.Slider(
							label='Motion Threshold',
							value=1,
							minimum=0,
							maximum=10,
							step=0.5,
							info='Threshold for the motion detection algorithm to consider a frame as different',
							interactive=False
						)
			SubmitButton = App.Button('πŸš€ Upscale Video')

		with App.Column(show_progress=True):
			with App.Group():
				OutputVideo = App.Video(
					label='Output Video', height=300, interactive=False, format=None
				)
			OutputDownload = App.DownloadButton(
				label='πŸ’Ύ Download Video', interactive=False
			)

	def ToggleRegionInputs(UseRegions):
		return (
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions),
			App.update(interactive=UseRegions)
		)

	InputUseRegions.change(
		fn=ToggleRegionInputs,
		inputs=[InputUseRegions],
		outputs=[InputThreshold, InputMinPercentage, InputMaxRectangles, InputPadding, InputSegmentRows, InputSegmentColumns, InputFullFrameInterval, InputMotionThreshold],
	)

	SubmitButton.click(
		fn=Upscaler().Process,
		inputs=[
			InputVideo,
			InputModel,
			InputUseRegions,
			InputThreshold,
			InputMinPercentage,
			InputMaxRectangles,
			InputPadding,
			InputSegmentRows,
			InputSegmentColumns,
			InputFullFrameInterval,
			InputMotionThreshold
		],
		outputs=[OutputVideo, OutputDownload],
	)

if __name__ == '__main__':
	os.makedirs(ModelDir, exist_ok=True)
	os.makedirs(TempDir, exist_ok=True)
	Logger.info('πŸš€ Starting Video Upscaler')
	Interface.launch(pwa=True)