From f836e7c7ba8f3211a3280190d679c2459d64d069 Mon Sep 17 00:00:00 2001 From: Paul Aumann Date: Sun, 24 Mar 2024 20:09:46 +0100 Subject: [PATCH] Initial commit --- .gitignore | 160 +++++ app/app.py | 226 +++++++ app/convert.py | 50 ++ app/params.py | 77 +++ app/preview.py | 151 +++++ app/requirements.txt | 4 + .../frame_generation_comparison.ipynb | 151 +++++ experimental/imgdiff_comparison.ipynb | 576 ++++++++++++++++++ experimental/slides.ipynb | 253 ++++++++ experimental/slides.py | 22 + 10 files changed, 1670 insertions(+) create mode 100755 .gitignore create mode 100755 app/app.py create mode 100755 app/convert.py create mode 100755 app/params.py create mode 100755 app/preview.py create mode 100755 app/requirements.txt create mode 100755 experimental/frame_generation_comparison.ipynb create mode 100755 experimental/imgdiff_comparison.ipynb create mode 100755 experimental/slides.ipynb create mode 100755 experimental/slides.py diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/app/app.py b/app/app.py new file mode 100755 index 0000000..377720e --- /dev/null +++ b/app/app.py @@ -0,0 +1,226 @@ +import tkinter as tk +from pathlib import Path +from tkinter import filedialog, messagebox + +import cv2 +import ttkbootstrap as ttk +from convert import differences, load_frames, select_frames +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.figure import Figure +from params import ParameterTuner +from PIL import Image, ImageTk +from preview import SlidePreview + + +class SlideConversionApp: + def __init__(self): + self._root = tk.Tk() + self._root.title("PDF Extraktion") + self._root.geometry("1400x1000") + + self._frames = None + self._diffs = None + + # file selection + self._file_selection = tk.Frame(self._root) + self._file_selection_label = tk.Label(self._file_selection, text="Videodatei:") + self._file_selection_label.pack() + self._file_selection_button = ttk.Button( + self._file_selection, text="Datei auswählen", command=self._select_file + ) + self._file_selection_button.pack() + self._file_selection.pack() + self._file = None + ttk.Separator(self._root, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10, padx=10) + + # slide selection + self._selection = tk.Frame(self._root) + self._params = ParameterTuner(self._selection) + self._param_threshold = self._params.add("Schwellenwert", 10, 1, 20) + self._params.pack(side=tk.LEFT, padx=10) + self._show_diffs_btn = ttk.Button(self._selection, text="Unterschiede anzeigen", command=self._show_diffs) + self._show_diffs_btn.pack(side=tk.LEFT, padx=10) + self._add_selected_btn = ttk.Button(self._selection, text="Folien hinzufügen", command=self._add_selection) + self._add_selected_btn.pack(side=tk.LEFT, padx=10) + self._selection.pack() + ttk.Separator(self._root, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10, padx=10) + + # generated images + self._images = SlidePreview(self._root, columns=6) + self._images.pack(fill=tk.BOTH, padx=10, expand=True) + ttk.Separator(self._root, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10, padx=10) + + # buttons + self._buttons = tk.Frame(self._root) + self._buttons.pack(side=tk.RIGHT, pady=(0, 5)) + self._button_create_pdf = ttk.Button(self._buttons, text="PDF erstellen", command=self._create_pdf) + self._button_create_pdf.pack(side=tk.RIGHT, padx=10) + self._button_add_frame = ttk.Button(self._buttons, text="Manuell hinzufügen", command=self._add_manually) + self._button_add_frame.pack(side=tk.RIGHT, padx=10) + + self._set_interaction_state(tk.DISABLED) + + self._root.mainloop() + + def _select_file(self): + file = filedialog.askopenfilename( + title="Videodatei auswählen", + filetypes=[ + ("Videodateien", "*.mp4"), + ("Alle Dateien", "*.*"), + ], + ) + if file is not None and file != "": + file = Path(file) + self._file_selected(file) + + def _file_selected(self, file: Path): + # set variables + cap = cv2.VideoCapture(file.as_posix()) + self._frames = None + + # analyze file + fps = int(cap.get(cv2.CAP_PROP_FPS)) + num_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + # load frames + status = tk.Toplevel(self._root) + status.title("Lade Videoframes ..") + status.grab_set() + status_label = tk.Label(status, text=f"Lade Videoframes (0/{num_frames})") + status_label.pack(padx=10, pady=10) + status_pb = ttk.Progressbar(status, bootstyle="progress", orient=tk.HORIZONTAL, length=200, mode="determinate") + status_pb.pack(padx=10, pady=10) + + def callback(text: str, frame_no: int, max_no: int): + status_label.configure(text=f"{text} ({frame_no}/{max_no})") + status_pb.configure(value=int((frame_no / max_no) * 100)) + status_label.update() + status_pb.update() + status_pb.update_idletasks() + + frames = load_frames(cap, fps, lambda f: callback("Lade Videoframes", f, num_frames)) + + diffs = differences(frames, lambda f: callback("Berechne Unterschiede", f, len(frames))) + + status.grab_release() + status.destroy() + + # update values & parameters + self._frames = frames + self._diffs = diffs + self._params.get_slider(self._param_threshold).configure(to=int(max(diffs))) + self._file_selection_button.configure(text=file.name) + self._file = file + self._images.clear() + self._set_interaction_state(tk.NORMAL) + + def _set_interaction_state(self, state: str): + elements = [self._button_create_pdf, self._show_diffs_btn, self._add_selected_btn, self._button_add_frame] + + self._params.set_state(state) + for element in elements: + element.configure(state=state) + + def _show_diffs(self): + if self._diffs is None: + return + + diffs = tk.Toplevel(self._root) + diffs.title("Unterschiede zwischen Frames") + diffs.grab_set() + fig = Figure(figsize=(10, 5), dpi=100) + plot = fig.add_subplot(111) + plot.plot(self._diffs) + canvas = FigureCanvasTkAgg(fig, master=diffs) + canvas.draw() + canvas.get_tk_widget().pack() + toolbar = NavigationToolbar2Tk(canvas, diffs) + toolbar.update() + canvas.get_tk_widget().pack() + + def _add_selection(self): + self._images.clear() + images = select_frames(self._frames, self._diffs, threshold=self._params.get(self._param_threshold)) + for image in images: + self._images.add(image) + + def _create_pdf(self): + if len(self._images) == 0: + messagebox.showerror(title="PDF erstellen", message="Keine Folien vorhanden.") + return + + file = filedialog.asksaveasfilename( + title="Speichern Unter", + filetypes=[ + ("PDF", "*.pdf"), + ("Alle Dateien", "*.*"), + ], + ) + + if file is not None and file != "": + file = Path(file).with_suffix(".pdf") + else: + return + + images = self._images.get_images() + images[0].save(file, "PDF", resolution=100.0, save_all=True, append_images=images[1:]) + messagebox.showinfo(title="PDF erstellen", message="Erfolgreich!") + + def _add_manually(self): + if self._images._selected_index == -1: + messagebox.showerror( + title="Manuelles Hinzufügen", + message="Wähle zunächst eine Folie zum dahinter einfügen aus", + ) + return + + frameview = tk.Toplevel(self._root) + frameview.title("Manuell Hinzufügen") + frameview.grab_set() + + thumbnail = tk.Label(frameview) + thumbnail.pack(padx=10, pady=5) + + def show(frame_no: int): + img = Image.fromarray(self._frames[frame_no][:, :, ::-1]) + img.thumbnail(size=(512, 512)) + thumbnail.current = ImageTk.PhotoImage(img) + thumbnail.configure(image=thumbnail.current) + + show(0) + + selected = tk.IntVar(value=0) + selected.trace_add("write", lambda v, i, m: show(selected.get())) + + framesel = tk.Frame(frameview) + framesel.pack(padx=10, pady=5) + back_btn = ttk.Button( + framesel, + text="<", + command=lambda: selected.set(0 if selected.get() - 1 < 0 else selected.get() - 1), + ) + next_btn = ttk.Button( + framesel, + text=">", + command=lambda: selected.set( + len(self._frames) - 1 if selected.get() + 1 >= len(self._frames) else selected.get() + 1 + ), + ) + slider = ttk.Scale(framesel, from_=0, to=len(self._frames) - 1, variable=selected, length=300) + back_btn.pack(side=tk.LEFT) + slider.pack(side=tk.LEFT, padx=5) + next_btn.pack(side=tk.LEFT) + + def add_image(): + self._images.insert( + Image.fromarray(self._frames[selected.get()][:, :, ::-1]), + self._images._selected_index + 1, + ) + + add_btn = ttk.Button(frameview, text="Hinzufügen", command=add_image) + add_btn.pack(pady=5) + + +if __name__ == "__main__": + app = SlideConversionApp() diff --git a/app/convert.py b/app/convert.py new file mode 100755 index 0000000..929cfdf --- /dev/null +++ b/app/convert.py @@ -0,0 +1,50 @@ +from typing import Callable + +import cv2 +import numpy as np +from PIL import Image +from tqdm import tqdm, trange + + +def compare(frame1, frame2): + absdiff = cv2.absdiff(frame1, frame2) + return np.mean(absdiff) + + +def load_frames(cap: cv2.VideoCapture, frame_interval: int, progress: Callable) -> list: + frames = [] + num_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + i = 0 + res = cap.grab() + while i < num_frames: + if res: + frames.append(cap.retrieve()[1]) + for _ in range(frame_interval): + res = cap.grab() + i += frame_interval + progress(i) + + return frames + + +def differences(frames: list, progress: Callable) -> list[float]: + diffs = [] + + for index, (frame1, frame2) in enumerate(zip(frames[:-1], frames[1:]), start=1): + diff = compare(frame1, frame2) + diffs.append(diff) + progress(index) + + return diffs + + +def select_frames(frames: list, diffs: list, threshold: float) -> list[Image.Image]: + selected_frames = [frames[0]] + + for frame, diff in zip(frames[1:], diffs, strict=True): + if diff > threshold: + selected_frames.append(frame) + + # Convert to Images + return [Image.fromarray(frame[:, :, ::-1]) for frame in selected_frames] diff --git a/app/params.py b/app/params.py new file mode 100755 index 0000000..60f6c6a --- /dev/null +++ b/app/params.py @@ -0,0 +1,77 @@ +import tkinter as tk +from dataclasses import dataclass + +import ttkbootstrap as ttk + + +@dataclass +class Parameter: + var: tk.IntVar | tk.DoubleVar + slider: ttk.Scale + + +class ParameterTuner(tk.Frame): + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, *kwargs) + + self._params: dict[int, Parameter] = {} + + def add(self, name: str, value: int | float, min_value: int | float, max_value: int | float) -> int: + frame = tk.Frame(self, width=200) + + if isinstance(value, int): + var = tk.IntVar(frame, value=value) + labelformat = "{name}: {value}" + else: + var = tk.DoubleVar(frame, value=value) + labelformat = "{name}: {value:.2f}" + + label = tk.Label(frame, text=f"{name}: {var.get()}") + slider = ttk.Scale( + frame, + variable=var, + from_=min_value, + to=max_value, + length=200, + orient=tk.HORIZONTAL, + ) + + var.trace_add( + "write", + lambda v, index, mode: label.configure(text=labelformat.format(name=name, value=var.get())), + ) + + label.pack() + slider.pack() + + frame.pack(side=tk.RIGHT, padx=10) + + param_id = len(self._params) + self._params[param_id] = Parameter(var, slider) + return param_id + + def get(self, param_id: int) -> int | float: + return self._params[param_id].var.get() + + def set(self, param_id: int, value: int | float): + self._params[param_id].var.set(value) + + def get_slider(self, param_id: int) -> ttk.Scale: + return self._params[param_id].slider + + def set_state(self, state: str, param_id: int = None): + if param_id is not None: + self._params[param_id].slider.configure(state=state) + else: + for param in self._params.values(): + param.slider.configure(state=state) + + +if __name__ == "__main__": + main = tk.Tk() + pt = ParameterTuner(main) + param1 = pt.add("Frameabstand", 30, 1, 60) + param2 = pt.add("Schwellenwert", 0.2, 0, 5) + pt.pack() + + main.mainloop() diff --git a/app/preview.py b/app/preview.py new file mode 100755 index 0000000..4cee812 --- /dev/null +++ b/app/preview.py @@ -0,0 +1,151 @@ +import tkinter as tk +from dataclasses import dataclass + +import ttkbootstrap as ttk +from PIL import Image, ImageTk +from ttkbootstrap.scrolled import ScrolledFrame + + +class Slide(tk.Frame): + def __init__(self, preview, image: Image.Image): + super().__init__(preview._frame) + self.preview = preview + + self.image = image + self.imagetk = ImageTk.PhotoImage(image) + + self.label = tk.Label(self, image=self.imagetk, borderwidth=1, relief=tk.SOLID) + self.label.pack(fill=tk.X, pady=(0, 5)) + + self.delbutton = ttk.Button(self, text="Löschen", bootstyle="danger-outline", command=self._on_delete) + self.delbutton.pack(fill=tk.X) + + self._is_selected = False + self.highlight = None + + self._configure_id = None + self.bind("", self._on_resize) + self.label.bind("", self._on_select) + + def _on_resize(self, e: tk.Event): + if self._configure_id != None: + self.after_cancel(self._configure_id) + self._configure_id = self.after(100, self._on_final_resize) + + def _on_select(self, e: tk.Event): + if self._is_selected: + self._on_unselect() + else: + self._is_selected = True + self.highlight = tk.Label(self, text="Ausgewählt", bg="green") + self.highlight.place(x=0, y=0) + self.preview.notify_selection(self) + + def _on_unselect(self): + self._is_selected = False + if self.highlight is not None: + self.highlight.destroy() + + def _on_final_resize(self): + thumbnail = self.image.copy() + thumbnail.thumbnail(size=(self.winfo_width(), self.image.height), resample=Image.NEAREST) + self.imagetk = ImageTk.PhotoImage(thumbnail) + + self.label.configure(image=self.imagetk) + self._configure_id = None + + def _on_delete(self): + self.preview.remove(self) + + +class SlidePreview(ScrolledFrame): + def __init__(self, parent, *args, columns: int = 4, **kwargs): + super().__init__(parent, *args, **kwargs) + + self._frame = tk.Frame(self) + self._frame.pack(fill=tk.BOTH, expand=True) + + self.columns = columns + for column in range(columns): + self._frame.grid_columnconfigure(column, weight=1) + + self._selected_index = -1 + self._slides = [] + + def _on_resize(self, e: tk.Event): + slide = self._slides[0] + slide.grid_forget() + slide.grid(row=0, column=0, sticky=tk.EW) + + def notify_selection(self, selected: Slide): + self._selected_index = self._slides.index(selected) + for slide in self._slides: + if slide is not selected: + slide._on_unselect() + + def notify_unselected(self, unselected: Slide): + if self._slides.index(unselected) == self._selected_index: + self._selected_index = -1 + + def add(self, slide: Image.Image): + index = len(self._slides) + + s = Slide(self, slide) + s.grid(row=index // self.columns, column=index % self.columns, sticky=tk.NSEW, padx=10, pady=10) + + self._slides.append(s) + + def insert(self, slide: Image.Image, index: int): + s = Slide(self, slide) + self._slides.insert(index, s) + + for index, slide in enumerate(self._slides): + slide.grid_forget() + slide.grid(row=index // self.columns, column=index % self.columns, sticky=tk.NSEW, padx=10, pady=10) + slide._on_final_resize() + + if index <= self._selected_index: + self._selected_index += 1 + + def remove(self, slide: Slide): + index = self._slides.index(slide) + if index < 0: + return + + if index == self._selected_index: + self._selected_index = -1 + elif index < self._selected_index: + self._selected_index -= 1 + + slide.destroy() + del self._slides[index] + + for index, slide in enumerate(self._slides[index:], start=index): + slide.grid_forget() + slide.grid(row=index // self.columns, column=index % self.columns, sticky=tk.NSEW, padx=10, pady=10) + + def clear(self): + for slide in self._slides: + slide.destroy() + self._slides = [] + self._selected_index = -1 + + def __len__(self): + return len(self._slides) + + def get_images(self) -> list[Image.Image]: + return [slide.image for slide in self._slides] + + +if __name__ == "__main__": + main = tk.Tk() + main.geometry("800x400") + + sp = SlidePreview(main) + sp.pack(fill=tk.BOTH, expand=True) + + for filename in [f"{i:02}" for i in range(16)]: + img = Image.open(f"machine learning in der radiologie_5ebbrbz/{filename}.png") + sp.add(img) + + main.mainloop() diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100755 index 0000000..7b0a694 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,4 @@ +opencv-python +ttkbootstrap +tqdm +matplotlib \ No newline at end of file diff --git a/experimental/frame_generation_comparison.ipynb b/experimental/frame_generation_comparison.ipynb new file mode 100755 index 0000000..c94ec77 --- /dev/null +++ b/experimental/frame_generation_comparison.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "from tqdm import tqdm, trange\n", + "from PIL import Image, ImageChops" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "VIDEO_FILE = \"diagnostik_aorta_gefaesse_fall8.mp4\"\n", + "cap = cv2.VideoCapture(VIDEO_FILE)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def _load_frames_sequentially(cap: cv2.VideoCapture, frame_interval: int):\n", + " frames = []\n", + " for i in range(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))):\n", + " res, frame = cap.read()\n", + " if i % frame_interval == 0 and res:\n", + " frames.append(frame)\n", + " return frames" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def _load_frames_random_access(cap: cv2.VideoCapture, frame_interval: int):\n", + " frames = []\n", + " total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n", + " i = 0\n", + " while i < total_frames:\n", + " cap.set(cv2.CAP_PROP_POS_FRAMES, i)\n", + " res, frame = cap.read()\n", + " if res:\n", + " frames.append(frame)\n", + " i += frame_interval\n", + " return frames" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "def _load_frames_ra_grab(cap: cv2.VideoCapture, frame_interval: int):\n", + " frames = []\n", + " total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n", + " i = 0\n", + " res = cap.grab()\n", + " while i < total_frames:\n", + " if res:\n", + " frames.append(cap.retrieve()[1])\n", + " for _ in range(frame_interval):\n", + " res = cap.grab()\n", + " i += frame_interval\n", + " return frames" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def load_frames(cap: cv2.VideoCapture, frame_interval: int, method: str):\n", + " if method == \"sequentially\":\n", + " return _load_frames_sequentially(cap, frame_interval)\n", + " elif method == \"random-access\":\n", + " return _load_frames_random_access(cap, frame_interval)\n", + " elif method == \"ra-grab\":\n", + " return _load_frames_ra_grab(cap, frame_interval)\n", + " else:\n", + " raise ValueError(\"Unknown method\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "method='ra-grab', duration=6.1883978843688965\n", + "method='random-access', duration=14.68180513381958\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "for method in (\"ra-grab\", \"random-access\"):\n", + " cap = cv2.VideoCapture(VIDEO_FILE)\n", + " start = time.time()\n", + " frames = load_frames(cap, 30, method)\n", + " print(f\"{method=}, duration={time.time() - start}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Resultat: Random Access und nur Dekodieren wenn benötigt ist am schnellsten" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.1" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/experimental/imgdiff_comparison.ipynb b/experimental/imgdiff_comparison.ipynb new file mode 100755 index 0000000..957e320 --- /dev/null +++ b/experimental/imgdiff_comparison.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "from tqdm import tqdm, trange\n", + "from PIL import Image, ImageChops" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "VIDEO_FILE = \"diagnostik_aorta_gefaesse_fall8.mp4\"\n", + "cap = cv2.VideoCapture(VIDEO_FILE)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def _load_frames_ra_grab(cap: cv2.VideoCapture, frame_interval: int):\n", + " frames = []\n", + " total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n", + " i = 0\n", + " res = cap.grab()\n", + " while i < total_frames:\n", + " if res:\n", + " frames.append(cap.retrieve()[1])\n", + " for _ in range(frame_interval):\n", + " res = cap.grab()\n", + " i += frame_interval\n", + " return frames" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "frames = _load_frames_ra_grab(cap, 30)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def pixeldiff(frame1, frame2):\n", + " diff = frame1 - frame2\n", + " return diff.sum() / (255 * np.prod(diff.shape))" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "def cv2diff(frame1, frame2):\n", + " absdiff = cv2.absdiff(frame1, frame2)\n", + " return np.mean(absdiff)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def select_frames(frames: list, threshold: float, compare_func):\n", + " selected_frames = [frames[0]]\n", + " compare_frame = frames[0]\n", + "\n", + " for i, frame in enumerate(frames[1:]):\n", + " diff = compare_func(compare_frame, frame)\n", + " if diff > threshold:\n", + " compare_frame = frame\n", + " selected_frames.append(compare_frame)\n", + "\n", + " # Convert to Images\n", + " return selected_frames" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "diffs = []\n", + "\n", + "for frame1, frame2 in zip(frames[:-1], frames[1:]):\n", + " diff = cv2diff(frame1, frame2)\n", + " diffs.append(diff)\n", + "\n", + "import matplotlib.pyplot as plt\n", + "plt.plot(diffs)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + "
\n", + "

0

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

1

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

2

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

3

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

4

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

5

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + "
\n", + "
\n", + "

6

\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import ipyplot\n", + "\n", + "for compare_func in (cv2diff,):\n", + " print(compare_func)\n", + " images = select_frames(frames, 10, compare_func)\n", + "\n", + " ipyplot.plot_images(images)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.1" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/experimental/slides.ipynb b/experimental/slides.ipynb new file mode 100755 index 0000000..0665eb8 --- /dev/null +++ b/experimental/slides.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "from tqdm import tqdm\n", + "from PIL import Image, ImageChops" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "VIDEO_FILE = \"thorax_wiederholung_theorie_2_edit.mp4\"" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "cap = cv2.VideoCapture(VIDEO_FILE)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "60" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fps = int(cap.get(cv2.CAP_PROP_FPS))\n", + "fps" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 36788/36788 [03:05<00:00, 198.49it/s]\n" + ] + } + ], + "source": [ + "frames = []\n", + "total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n", + "\n", + "for i in tqdm(range(total_frames)):\n", + " ret, frame = cap.read()\n", + " if i % fps == 0 and ret:\n", + " frames.append(frame)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "images = [ Image.fromarray(frame[:,:,::-1]) for frame in frames]" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "thumbnails = [ img.copy() for img in images]\n", + "for img in thumbnails:\n", + " img.thumbnail((64,64))" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "def compare(frame1, frame2):\n", + " diff = frame1 - frame2\n", + " return diff.sum() / (255 * np.prod(diff.shape))" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "def compare(image1, image2):\n", + " diff = ImageChops.difference(image1, image2)\n", + " return diff.histogram()" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "list" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(compare(thumbnails[0], thumbnails[10]))" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 613/613 [00:06<00:00, 95.81it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 73 slides.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "diffs = []\n", + "threshold = 0.23\n", + "\n", + "selected_frames = [frames[0]]\n", + "\n", + "compare_frame = frames[0]\n", + "for frame in tqdm(frames[1:]):\n", + " diff = compare(compare_frame, frame)\n", + " diffs.append(diff)\n", + " if diff > threshold:\n", + " compare_frame = frame\n", + " selected_frames.append(frame)\n", + "\n", + "print(f\"Found {len(selected_frames)} slides.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "73" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(selected_frames)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "73it [00:24, 3.02it/s]\n" + ] + } + ], + "source": [ + "from PIL import Image\n", + "from os import makedirs\n", + "\n", + "imgdir = VIDEO_FILE.removesuffix('.mp4')\n", + "\n", + "makedirs(imgdir, exist_ok=True)\n", + "\n", + "\n", + "for index,frame in tqdm(enumerate(selected_frames)):\n", + " im = Image.fromarray(frame[:,:,::-1], mode=\"RGB\")\n", + " im.save(f\"{imgdir}/{index:02}.png\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.1" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/experimental/slides.py b/experimental/slides.py new file mode 100755 index 0000000..8ffbefa --- /dev/null +++ b/experimental/slides.py @@ -0,0 +1,22 @@ +import argparse +from pathlib import Path + +import cv2 + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("video", type=Path, help="Video file") + parser.add_argument("--interval", "-i", type=int, default=1, help="Frame intervals") + args = parser.parse_args() + + cap = cv2.VideoCapture(args.video.as_posix()) + fps = cap.get(cv2.CAP_PROP_FPS) + + frames = [] + f = 0 + while f < cap.get(cv2.CAP_PROP_FRAME_COUNT): + cap.set(cv2.CAP_PROP_POS_FRAMES, f) + frames.append(cap.read()) + f += int(fps) + + print(len(frames))