Program za poboljšanje kvaliteta video snimaka

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:

  1. Pokrenite program: python3 video_app.py
  2. Kliknite na “Učitaj video” i izaberite željeni fajl.
  3. Podesite željene parametre pomoću klizača.
  4. Pregledajte rezultat u realnom vremenu (za izabrani frejm).
  5. 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()

By Abel

Leave a Reply

Your email address will not be published. Required fields are marked *