Ovaj Python GUI program omogućava korisnicima da poboljšaju kvalitet videa pomoću detaljnih parametara obrade slike. Namenjen je svima koji žele da optimizuju oštrinu, kontrast, svetlinu, zasićenost i ekspoziciju video materijala na jednostavan i vizuelno intuitivan način. Program je pre svega namenjen starim video snimcima digitalizovanim sa traka do formata 640x480p.
🎥 Glavne mogućnosti:
- Učitavanje video fajlova: podržava
.mp4
,.avi
i.mov
. - Vizuelni prikaz: originalni i obrađeni frejm prikazani su jedan pored drugog.
- Podesivi parametri:
- Oštrina (sharpen)
- Kontrast
- Svetlina (brightness)
- Zasićenost (saturation)
- Izložnost / gama korekcija
- Skaliranje rezolucije (u procentima)
- Biranje određenog frejma za prikaz
- Export celog videa: nakon podešavanja željenih parametara, kompletan video se može izvesti u novom MP4 fajlu.
🧭 Kako se koristi:
- Pokrenite program:
python3 video_app.py
- Kliknite na “Učitaj video” i izaberite željeni fajl.
- Podesite željene parametre pomoću klizača.
- Pregledajte rezultat u realnom vremenu (za izabrani frejm).
- Kada ste zadovoljni, kliknite “Exportuj ceo video” i sačuvajte rezultat.
Program koristi biblioteke: OpenCV
, Pillow
, Tkinter
, NumPy
.
Evo jednostavnog Bash skripta koji instalira sve potrebne zavisnosti za pokretanje ovog programa na Linuxu (Debian/Ubuntu sistem):
#!/bin/bash echo "Instalacija zavisnosti za VideoSharpenApp..." # Ažuriranje liste paketa sudo apt update # Instalacija sistemskih biblioteka sudo apt install -y python3 python3-pip python3-tk python3-opencv libgl1-mesa-glx # Instalacija Python biblioteka pip3 install --upgrade pip pip3 install numpy opencv-python pillow echo "Instalacija završena."
Programski kod:
# The MIT License (MIT) # Copyright (c) 2025 Aleksandar Maričić # # Ovim se omogućava bilo kome da koristi, kopira, menja, spaja, objavljuje, # distribuira, daje podlicencu i/ili prodaje kopije ovog softverskog programa, # uz uslov da u svim kopijama ili značajnim delovima softverskog programa bude # uključena sledeća obavest: # # Copyright (c) 2025 Aleksandar Maričić # # Ovaj softverski program je pružen "takav kakav jeste", bez bilo kakvih garancija, # izričitih ili impliciranih, uključujući, ali ne ograničavajući se na, garancije o # prikladnosti za prodaju ili pogodnosti za određenu svrhu. U svakom slučaju, autori # ili nosioci prava nisu odgovorni za bilo kakvu štetu ili druge obaveze koje mogu nastati # usled upotrebe ovog softverskog programa. # Naziv programa: Program za poboljšanje kvaliteta video snimaka import cv2 import numpy as np import tkinter as tk from tkinter import filedialog, messagebox from tkinter import ttk from PIL import Image, ImageTk, ImageEnhance def sharpen_image(image, strength): kernel_value = strength kernel = np.array([ [0, -1, 0], [-1, kernel_value, -1], [0, -1, 0] ]) return cv2.filter2D(image, -1, kernel) def adjust_contrast(image, factor): enhancer = ImageEnhance.Contrast(image) return enhancer.enhance(factor) def adjust_brightness(image, factor): enhancer = ImageEnhance.Brightness(image) return enhancer.enhance(factor) def adjust_saturation(image, factor): enhancer = ImageEnhance.Color(image) return enhancer.enhance(factor) def adjust_exposure(image, gamma): inv_gamma = 1.0 / gamma table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in np.arange(256)]).astype("uint8") return cv2.LUT(image, table) class VideoSharpenApp: def __init__(self, root): self.root = root self.root.title("Video poboljšanje slike - detaljni parametri") # Frame za prikaz slika self.image_frame = tk.Frame(root) self.image_frame.pack(pady=5) self.original_panel = tk.Label(self.image_frame, text="Original") self.original_panel.pack(side="left", padx=10, pady=5) self.processed_panel = tk.Label(self.image_frame, text="Obrađeno") self.processed_panel.pack(side="right", padx=10, pady=5) # Scrollable frame za kontrole control_container = tk.Frame(root) control_container.pack(fill='both', expand=True) canvas = tk.Canvas(control_container) canvas.pack(side='left', fill='both', expand=True) scrollbar = ttk.Scrollbar(control_container, orient='vertical', command=canvas.yview) scrollbar.pack(side='right', fill='y') canvas.configure(yscrollcommand=scrollbar.set) self.controls_frame = tk.Frame(canvas) canvas.create_window((0, 0), window=self.controls_frame, anchor='nw') def on_frame_configure(event): canvas.configure(scrollregion=canvas.bbox("all")) self.controls_frame.bind("<Configure>", on_frame_configure) # Klizači i dugmad (raspoređeni u mrežu 3 kolone) # Kreiramo listu sa parametrima da ih lakše rasporedimo sliders_info = [ ("Oštrina (1.00 - 15.00)", 100, 1500, 500), ("Kontrast (0.5 - 3.0)", 50, 300, 100), ("Svetlina (0.5 - 3.0)", 50, 300, 100), ("Zasićenost (0.0 - 3.0)", 0, 300, 100), ("Izložnost/Gama (0.1 - 3.0)", 10, 300, 100), ("Frejm", 0, 100, 0), ("Skaliranje rezolucije (%)", 50, 300, 100) ] self.sliders = [] # Napravi grid 3 kolone for i, (label, frm, to, default) in enumerate(sliders_info): scale = tk.Scale(self.controls_frame, from_=frm, to=to, orient=tk.HORIZONTAL, label=label, resolution=1 if label != "Frejm" else 1, command=self.update_frame_preview) scale.set(default) row = i // 3 col = i % 3 scale.grid(row=row, column=col, sticky='ew', padx=5, pady=5) self.controls_frame.grid_columnconfigure(col, weight=1) self.sliders.append(scale) # Posebno dugmad ispod klizača btn_frame = tk.Frame(self.controls_frame) btn_frame.grid(row=(len(sliders_info) + 2) // 3, column=0, columnspan=3, pady=10, sticky='ew') self.load_button = tk.Button(btn_frame, text="Učitaj video", command=self.load_video) self.load_button.pack(side='left', expand=True, fill='x', padx=5) self.export_button = tk.Button(btn_frame, text="Exportuj ceo video", command=self.export_video) self.export_button.pack(side='left', expand=True, fill='x', padx=5) # Interni podaci self.video_path = "" self.cap = None self.total_frames = 0 self.fps = 25 self.frame_width = 0 self.frame_height = 0 def load_video(self): path = filedialog.askopenfilename(filetypes=[("Video fajlovi", "*.mp4 *.avi *.mov")]) if not path: return self.video_path = path self.cap = cv2.VideoCapture(path) if not self.cap.isOpened(): messagebox.showerror("Greška", "Ne mogu da otvorim video.") return self.fps = self.cap.get(cv2.CAP_PROP_FPS) self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) self.sliders[5].configure(to=max(0, self.total_frames - 1)) # Frejm slider self.sliders[5].set(0) self.update_frame_preview() # Podesi veličinu prozora da stane dve slike + kontrole width = self.frame_width * 2 + 60 height = self.frame_height + 220 self.root.geometry(f"{width}x{height}") def get_frame_at(self, index): if not self.cap: return None self.cap.set(cv2.CAP_PROP_POS_FRAMES, index) ret, frame = self.cap.read() if not ret: return None return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) def update_frame_preview(self, event=None): if not self.cap: return frame_idx = self.sliders[5].get() sharpness = self.sliders[0].get() / 100.0 contrast = self.sliders[1].get() / 100.0 brightness = self.sliders[2].get() / 100.0 saturation = self.sliders[3].get() / 100.0 exposure = self.sliders[4].get() / 100.0 scale_percent = self.sliders[6].get() frame = self.get_frame_at(frame_idx) if frame is None: return # Prikaz originalnog frejma orig_img = Image.fromarray(frame) orig_photo = ImageTk.PhotoImage(orig_img) self.original_panel.configure(image=orig_photo) self.original_panel.image = orig_photo # Obrada slike processed = sharpen_image(frame, sharpness) processed = adjust_exposure(processed, exposure) pil_img = Image.fromarray(processed) pil_img = adjust_contrast(pil_img, contrast) pil_img = adjust_brightness(pil_img, brightness) pil_img = adjust_saturation(pil_img, saturation) # Skaliranje new_w = int(pil_img.width * scale_percent / 100) new_h = int(pil_img.height * scale_percent / 100) pil_img = pil_img.resize((new_w, new_h), Image.LANCZOS) proc_photo = ImageTk.PhotoImage(pil_img) self.processed_panel.configure(image=proc_photo) self.processed_panel.image = proc_photo def export_video(self): if not self.cap: messagebox.showerror("Greška", "Video nije učitan.") return sharpness = self.sliders[0].get() / 100.0 contrast = self.sliders[1].get() / 100.0 brightness = self.sliders[2].get() / 100.0 saturation = self.sliders[3].get() / 100.0 exposure = self.sliders[4].get() / 100.0 scale_percent = self.sliders[6].get() output_path = filedialog.asksaveasfilename(defaultextension=".mp4", filetypes=[("MP4 fajl", "*.mp4")]) if not output_path: return new_w = int(self.frame_width * scale_percent / 100) new_h = int(self.frame_height * scale_percent / 100) self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) total = int(self.total_frames) out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, (new_w, new_h)) print(f"Exportujem video u {output_path}...") for i in range(total): ret, frame = self.cap.read() if not ret: break rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) processed = sharpen_image(rgb_frame, sharpness) processed = adjust_exposure(processed, exposure) pil_img = Image.fromarray(processed) pil_img = adjust_contrast(pil_img, contrast) pil_img = adjust_brightness(pil_img, brightness) pil_img = adjust_saturation(pil_img, saturation) new_frame = pil_img.resize((new_w, new_h), Image.LANCZOS) new_frame_cv = cv2.cvtColor(np.array(new_frame), cv2.COLOR_RGB2BGR) out.write(new_frame_cv) if i % 30 == 0: print(f"{i}/{total} frejmova obrađeno...") out.release() messagebox.showinfo("Završeno", f"Video sačuvan:\n{output_path}") if __name__ == "__main__": root = tk.Tk() app = VideoSharpenApp(root) root.mainloop()