Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29a86d895a |
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(HDMIViewer VERSION 1.0.0 LANGUAGES CXX)
|
project(HDMIViewer VERSION 1.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -9,7 +9,8 @@ set(CMAKE_AUTOUIC ON)
|
|||||||
|
|
||||||
# Qt6 – Gui enthält den JPEG-Plugin-Support (QImage::fromData)
|
# Qt6 – Gui enthält den JPEG-Plugin-Support (QImage::fromData)
|
||||||
# OpenGLWidgets für das GPU-beschleunigte Video-Rendering (Bildglättung/Retro-Optik)
|
# OpenGLWidgets für das GPU-beschleunigte Video-Rendering (Bildglättung/Retro-Optik)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets)
|
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets Svg)
|
||||||
|
# Svg: Programm-Icon (assets/hdmi-viewer.svg) wird als SVG geladen
|
||||||
|
|
||||||
# ALSA
|
# ALSA
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -27,8 +28,10 @@ set(SOURCES
|
|||||||
src/MainWindow.cpp
|
src/MainWindow.cpp
|
||||||
src/VideoWidget.cpp
|
src/VideoWidget.cpp
|
||||||
src/DeviceDialog.cpp
|
src/DeviceDialog.cpp
|
||||||
|
src/Profile.cpp
|
||||||
src/AudioThread.cpp
|
src/AudioThread.cpp
|
||||||
src/CaptureThread.cpp
|
src/CaptureThread.cpp
|
||||||
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(hdmi-viewer ${SOURCES})
|
add_executable(hdmi-viewer ${SOURCES})
|
||||||
@@ -40,6 +43,7 @@ target_link_libraries(hdmi-viewer
|
|||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
Qt6::OpenGL
|
Qt6::OpenGL
|
||||||
Qt6::OpenGLWidgets
|
Qt6::OpenGLWidgets
|
||||||
|
Qt6::Svg
|
||||||
${ALSA_LIBRARIES}
|
${ALSA_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,3 +58,8 @@ target_compile_options(hdmi-viewer PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS hdmi-viewer DESTINATION bin)
|
install(TARGETS hdmi-viewer DESTINATION bin)
|
||||||
|
|
||||||
|
# Startmenü-Eintrag und Icon (Cinnamon/GNOME/KDE lesen beides aus share/)
|
||||||
|
install(FILES hdmi-viewer.desktop DESTINATION share/applications)
|
||||||
|
install(FILES assets/hdmi-viewer.svg
|
||||||
|
DESTINATION share/icons/hicolor/scalable/apps)
|
||||||
|
|||||||
4
PKGBUILD
4
PKGBUILD
@@ -1,12 +1,12 @@
|
|||||||
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
||||||
pkgname=hdmi-viewer
|
pkgname=hdmi-viewer
|
||||||
pkgver=1.0.0
|
pkgver=1.1.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Schlanker Qt6/C++ Live-Viewer für HDMI-Grabber (V4L2 + ALSA), Companion zu OBS Studio"
|
pkgdesc="Schlanker Qt6/C++ Live-Viewer für HDMI-Grabber (V4L2 + ALSA), Companion zu OBS Studio"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
url="https://git.projekt-hirnfrei.de/diabolus/hdmiviewer"
|
url="https://git.projekt-hirnfrei.de/diabolus/hdmiviewer"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('qt6-base' 'alsa-lib')
|
depends=('qt6-base' 'qt6-svg' 'alsa-lib')
|
||||||
makedepends=('cmake')
|
makedepends=('cmake')
|
||||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -45,24 +45,40 @@ Gedacht als Companion zu OBS Studio: **Anzeigen ohne Aufnehmen**.
|
|||||||
unabhängig vom Pixelformat.
|
unabhängig vom Pixelformat.
|
||||||
- **Roh-Modus** (Taste **R**): Diagnose-Umschalter, der sämtliche Verarbeitung
|
- **Roh-Modus** (Taste **R**): Diagnose-Umschalter, der sämtliche Verarbeitung
|
||||||
(Bildglättung, Retro-Optik, Studio-Range-Korrektur, Kontrast-/
|
(Bildglättung, Retro-Optik, Studio-Range-Korrektur, Kontrast-/
|
||||||
Sättigungs-Punch, Gamma) umgeht und das Bild 1:1 anzeigt.
|
Sättigungs-Punch, Gamma) umgeht und das Bild 1:1 anzeigt. Zusätzlich werden
|
||||||
|
die Hardware-Regler am Grabber-Chip auf ihren beim Start gemerkten
|
||||||
|
Ausgangswert zurückgesetzt; beim Ausschalten gelten wieder die gespeicherten
|
||||||
|
Werte bzw. das aktive Profil.
|
||||||
- **Live-Vergleich** (Taste **V**): zeigt links das rohe und rechts das
|
- **Live-Vergleich** (Taste **V**): zeigt links das rohe und rechts das
|
||||||
bearbeitete Bild nebeneinander – praktisch zum Fein-Tunen der Hardware-Regler.
|
bearbeitete Bild nebeneinander – praktisch zum Fein-Tunen der Hardware-Regler.
|
||||||
|
- **Profile** (Menü „Profile“, Tasten **F1**–**F9**): speichert die aktuelle
|
||||||
|
Kombination aus Bildglättung, Retro-Optik, Nachschärfen und den drei
|
||||||
|
Hardware-Reglern unter einem Namen (z. B. „PlayStation“, „PS3“). Beim
|
||||||
|
Umschalten werden Filter und Grabber-Regler sofort gesetzt; „unverändert“
|
||||||
|
bei einem Hardware-Regler setzt ihn auf den Ausgangswert des Chips zurück.
|
||||||
|
Speichern/Löschen geht im Menü „Profile“ und im Startdialog, dort lädt die
|
||||||
|
Auswahl die Werte in die Felder. Ein Häkchen markiert das Profil, das zu den
|
||||||
|
aktuellen Einstellungen passt. F1–F9 folgen der alphabetischen Reihenfolge
|
||||||
|
im Menü.
|
||||||
- **Screenshot** (Taste **P**): speichert das aktuell angezeigte (bearbeitete)
|
- **Screenshot** (Taste **P**): speichert das aktuell angezeigte (bearbeitete)
|
||||||
Frame als PNG im Home-Verzeichnis.
|
Frame als PNG im Home-Verzeichnis.
|
||||||
- **Frame-Dump** (Taste **D**, nur bei MJPEG): speichert das nächste rohe,
|
- **Frame-Dump** (Taste **D**, nur bei MJPEG): speichert das nächste rohe,
|
||||||
unveränderte MJPEG-Frame direkt vom Chip als JPEG – nützlich, um zu prüfen,
|
unveränderte MJPEG-Frame direkt vom Chip als JPEG – nützlich, um zu prüfen,
|
||||||
ob Abweichungen schon in den Rohdaten liegen oder erst bei der eigenen
|
ob Abweichungen schon in den Rohdaten liegen oder erst bei der eigenen
|
||||||
Verarbeitung entstehen.
|
Verarbeitung entstehen.
|
||||||
|
- **Programm-Icon und Startmenü-Eintrag**: Icon als SVG (`assets/hdmi-viewer.svg`),
|
||||||
|
dazu eine `.desktop`-Datei. Beides wird bei `cmake --install` bzw. über das
|
||||||
|
Paket installiert; das Fenster-Icon ist zusätzlich ins Programm eingebettet.
|
||||||
|
|
||||||
## Abhängigkeiten (Arch Linux)
|
## Abhängigkeiten (Arch Linux)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S qt6-base cmake alsa-lib base-devel
|
sudo pacman -S qt6-base qt6-svg cmake alsa-lib base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
`qt6-base` bringt auf Arch bereits die benötigten `Qt6OpenGL`- und
|
`qt6-base` bringt auf Arch bereits die benötigten `Qt6OpenGL`- und
|
||||||
`Qt6OpenGLWidgets`-Module mit. `linux/videodev2.h` ist Teil von
|
`Qt6OpenGLWidgets`-Module mit. `qt6-svg` wird für das SVG-Programm-Icon
|
||||||
|
benötigt. `linux/videodev2.h` ist Teil von
|
||||||
`linux-api-headers`, das über die `base`-Gruppe ohnehin installiert ist.
|
`linux-api-headers`, das über die `base`-Gruppe ohnehin installiert ist.
|
||||||
|
|
||||||
## Installation über AUR
|
## Installation über AUR
|
||||||
@@ -89,6 +105,12 @@ cmake .. -DCMAKE_BUILD_TYPE=Release
|
|||||||
make -j$(nproc)
|
make -j$(nproc)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional systemweit installieren (inkl. Startmenü-Eintrag und Icon):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cmake --install . --prefix /usr/local
|
||||||
|
```
|
||||||
|
|
||||||
## Starten
|
## Starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -107,16 +129,33 @@ make -j$(nproc)
|
|||||||
| , / . | Hardware-Helligkeit dunkler / heller |
|
| , / . | Hardware-Helligkeit dunkler / heller |
|
||||||
| 1 / 2 | Hardware-Kontrast runter / hoch |
|
| 1 / 2 | Hardware-Kontrast runter / hoch |
|
||||||
| 3 / 4 | Hardware-Sättigung runter / hoch |
|
| 3 / 4 | Hardware-Sättigung runter / hoch |
|
||||||
| R | Roh-Modus (Diagnose, alle Effekte umgangen) ein/aus |
|
| R | Roh-Modus (Diagnose, alle Filter + HW-Regler aus) ein/aus |
|
||||||
|
| F1–F9 | Profil 1–9 laden (alphabetisch, siehe Menü „Profile“) |
|
||||||
| V | Live-Vergleich (links roh, rechts bearbeitet) ein/aus |
|
| V | Live-Vergleich (links roh, rechts bearbeitet) ein/aus |
|
||||||
| P | Screenshot speichern (PNG) |
|
| P | Screenshot speichern (PNG) |
|
||||||
| D | Rohes MJPEG-Frame dumpen (Diagnose) |
|
| D | Rohes MJPEG-Frame dumpen (Diagnose) |
|
||||||
| Esc | Vollbild verlassen ODER zurück zum Gerätedialog |
|
| Esc | Vollbild verlassen ODER zurück zum Gerätedialog |
|
||||||
|
|
||||||
|
## Hilfsprogramme
|
||||||
|
|
||||||
|
`tools/colormatch.py` vergleicht zwei Screenshots (z. B. dasselbe Bild in einem
|
||||||
|
Referenzprogramm und in HDMI Viewer) und ermittelt pro Farbkanal Gain, Offset
|
||||||
|
und Gamma, die das Testbild an die Referenz angleichen – inklusive fertiger
|
||||||
|
GLSL-Zeile. Mit `--palette` werden zusätzlich die häufigsten Flächenfarben
|
||||||
|
beider Bilder ausgegeben. Benötigt `python-numpy` und `python-pillow`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/colormatch.py referenz.png test.png --palette
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Bilder müssen auf demselben Weg entstanden sein (z. B. beide als
|
||||||
|
Desktop-Screenshot des laufenden Programms).
|
||||||
|
|
||||||
## Einstellungen
|
## Einstellungen
|
||||||
|
|
||||||
Werden gespeichert in: `~/.config/hdmi-viewer/HDMIViewer.conf`
|
Werden gespeichert in: `~/.config/hdmi-viewer/HDMIViewer.conf`
|
||||||
(Standard XDG-Pfad, kein manuelles Anlegen nötig)
|
(Standard XDG-Pfad, kein manuelles Anlegen nötig). Profile liegen in
|
||||||
|
derselben Datei im Abschnitt `[profiles]`.
|
||||||
|
|
||||||
## Audio-Gerät finden
|
## Audio-Gerät finden
|
||||||
|
|
||||||
@@ -131,3 +170,22 @@ aplay /tmp/test.wav
|
|||||||
|
|
||||||
Das Gerät dann im Dialog so eingeben: z.B. `hw:2,0` oder `plughw:2,0`
|
Das Gerät dann im Dialog so eingeben: z.B. `hw:2,0` oder `plughw:2,0`
|
||||||
(plughw macht automatische Sample-Rate-Konvertierung, falls nötig)
|
(plughw macht automatische Sample-Rate-Konvertierung, falls nötig)
|
||||||
|
|
||||||
|
## Änderungen
|
||||||
|
|
||||||
|
### 1.1.0
|
||||||
|
|
||||||
|
- Profile: Kombinationen aus Bildglättung, Retro-Optik, Nachschärfen und
|
||||||
|
Hardware-Reglern speichern, laden und per **F1**–**F9** umschalten (Menü und
|
||||||
|
Startdialog)
|
||||||
|
- Fix: Das Bild wurde in der Anzeige auf etwa die Hälfte der Helligkeit
|
||||||
|
abgedunkelt (Weiß 255 → 128), auch im Roh-Modus. Ursache war OpenGL-Zustand
|
||||||
|
(u. a. Blending), den der `QPainter` der „Kein Signal“-Anzeige zurückließ.
|
||||||
|
Der Zustand wird jetzt vor dem Zeichnen des Videobilds zurückgesetzt.
|
||||||
|
- Roh-Modus (**R**) setzt zusätzlich die Hardware-Regler auf den Ausgangswert
|
||||||
|
- Neues Programm-Icon, Startmenü-Eintrag, neue Abhängigkeit `qt6-svg`
|
||||||
|
- Neues Hilfsprogramm `tools/colormatch.py`
|
||||||
|
|
||||||
|
### 1.0.0
|
||||||
|
|
||||||
|
- Erste Version
|
||||||
|
|||||||
54
assets/hdmi-viewer.svg
Normal file
54
assets/hdmi-viewer.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
|
<title>HDMI Viewer</title>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="screenGlow" cx="50%" cy="40%" r="65%">
|
||||||
|
<stop offset="0%" stop-color="#9cecf7" />
|
||||||
|
<stop offset="100%" stop-color="#62b8ea" />
|
||||||
|
</radialGradient>
|
||||||
|
<clipPath id="screenClip">
|
||||||
|
<rect x="140" y="137" width="232" height="162" rx="20" ry="20" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Hintergrund: abgerundetes Quadrat, ausserhalb transparent -->
|
||||||
|
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#1e1e2e" stroke="#cdd6f4" stroke-width="12" />
|
||||||
|
|
||||||
|
<g transform="translate(0,-8)">
|
||||||
|
<!-- HDMI-Stecker (hinter dem Monitor, sitzt direkt am Gehaeuse) -->
|
||||||
|
<g id="hdmi-plug">
|
||||||
|
<!-- Gehaeuse -->
|
||||||
|
<path d="M 198 330 L 314 330 L 314 380 L 298 398 L 214 398 L 198 380 Z"
|
||||||
|
fill="#45475a" stroke="#cdd6f4" stroke-width="10" stroke-linejoin="round" />
|
||||||
|
<!-- Magenta-Akzent -->
|
||||||
|
<rect x="203" y="362" width="106" height="14" fill="#d35fd0" />
|
||||||
|
<!-- Metall-Steckfahne: flaches Trapez mit abgeschraegten unteren Ecken -->
|
||||||
|
<path d="M 220 398 L 292 398 L 292 420 L 280 434 L 232 434 L 220 420 Z"
|
||||||
|
fill="#cdd6f4" stroke="#cdd6f4" stroke-width="6" stroke-linejoin="round" />
|
||||||
|
<rect x="238" y="408" width="36" height="9" rx="2" fill="#1e1e2e" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Roehrenmonitor: Gehaeuse -->
|
||||||
|
<rect x="112" y="112" width="288" height="228" rx="32" ry="32" fill="#181825" stroke="#cdd6f4" stroke-width="16" />
|
||||||
|
<!-- Blende -->
|
||||||
|
<rect x="128" y="124" width="256" height="188" rx="24" ry="24" fill="#313244" stroke="#11111b" stroke-width="6" />
|
||||||
|
<!-- Bildschirm -->
|
||||||
|
<rect x="140" y="137" width="232" height="162" rx="20" ry="20" fill="url(#screenGlow)" />
|
||||||
|
|
||||||
|
<g clip-path="url(#screenClip)">
|
||||||
|
<!-- 4 dicke Scanlines -->
|
||||||
|
<line x1="120" y1="164" x2="392" y2="164" stroke="#11111b" stroke-width="9" opacity="0.28" />
|
||||||
|
<line x1="120" y1="200" x2="392" y2="200" stroke="#11111b" stroke-width="9" opacity="0.28" />
|
||||||
|
<line x1="120" y1="236" x2="392" y2="236" stroke="#11111b" stroke-width="9" opacity="0.28" />
|
||||||
|
<line x1="120" y1="272" x2="392" y2="272" stroke="#11111b" stroke-width="9" opacity="0.28" />
|
||||||
|
<!-- dezenter Glanzreflex -->
|
||||||
|
<path d="M 145 142 L 262 142 L 145 222 Z" fill="#ffffff" opacity="0.10" />
|
||||||
|
<!-- Live-Punkt mit dunklem Ring -->
|
||||||
|
<circle cx="186" cy="182" r="27" fill="#11111b" />
|
||||||
|
<circle cx="186" cy="182" r="19" fill="#ff4d5e" />
|
||||||
|
<circle cx="180" cy="176" r="5" fill="#ffffff" opacity="0.6" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Innenkante des Bildschirms -->
|
||||||
|
<rect x="140" y="137" width="232" height="162" rx="20" ry="20" fill="none" stroke="#11111b" stroke-width="5" opacity="0.4" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
11
hdmi-viewer.desktop
Normal file
11
hdmi-viewer.desktop
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=HDMI Viewer
|
||||||
|
GenericName=HDMI-Grabber-Vorschau
|
||||||
|
Comment=Live-Vorschau für HDMI-Grabber
|
||||||
|
Exec=hdmi-viewer
|
||||||
|
Icon=hdmi-viewer
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=AudioVideo;Video;
|
||||||
|
Keywords=HDMI;Grabber;Capture;V4L2;Video;
|
||||||
|
StartupWMClass=HDMIViewer
|
||||||
74
hdmi_viewer_icon.md
Normal file
74
hdmi_viewer_icon.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<!-- Screen Gradient Glow -->
|
||||||
|
<radialGradient id="screenGlow" cx="50%" cy="40%" r="60%">
|
||||||
|
<stop offset="0%" stop-color="#a6e3a1" stop-opacity="0.1" />
|
||||||
|
<stop offset="0%" stop-color="#89dceb" />
|
||||||
|
<stop offset="100%" stop-color="#74c7ec" />
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<!-- Red Dot Glow -->
|
||||||
|
<radialGradient id="redGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#f38ba8" />
|
||||||
|
<stop offset="100%" stop-color="#f38ba8" stop-opacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<!-- Clip path for the screen interior (rounded CRT shape) -->
|
||||||
|
<clipPath id="screenClip">
|
||||||
|
<rect x="140" y="145" width="232" height="162" rx="20" ry="20" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 1. Rounded Square Background (Squircle) -->
|
||||||
|
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#1e1e2e" stroke="#cdd6f4" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- 2. HDMI Plug (Positioned below the monitor frame) -->
|
||||||
|
<g id="hdmi-plug">
|
||||||
|
<!-- Outer thick lavender outline border for the plug -->
|
||||||
|
<path d="M 200 328 L 312 328 L 312 375 L 292 405 L 220 405 L 200 375 Z" fill="#cdd6f4" stroke="#cdd6f4" stroke-width="12" stroke-linejoin="round" />
|
||||||
|
<!-- Plug Body -->
|
||||||
|
<path d="M 206 334 L 306 334 L 306 372 L 288 399 L 224 399 L 206 372 Z" fill="#313244" />
|
||||||
|
<!-- HDMI Metal Connector Tip -->
|
||||||
|
<path d="M 226 399 L 286 399 L 286 422 L 278 428 L 234 428 L 226 422 Z" fill="#cdd6f4" stroke="#cdd6f4" stroke-width="4" stroke-linejoin="round" />
|
||||||
|
<path d="M 230 403 L 282 403 L 282 420 L 276 424 L 236 424 L 230 420 Z" fill="#585b70" />
|
||||||
|
<!-- Connector Details / Pins region -->
|
||||||
|
<rect x="242" y="410" width="28" height="8" rx="2" fill="#181825" />
|
||||||
|
<!-- Plug Grip Lines -->
|
||||||
|
<line x1="240" y1="352" x2="272" y2="352" stroke="#45475a" stroke-width="5" stroke-linecap="round" />
|
||||||
|
<line x1="240" y1="364" x2="272" y2="364" stroke="#45475a" stroke-width="5" stroke-linecap="round" />
|
||||||
|
<line x1="240" y1="376" x2="272" y2="376" stroke="#45475a" stroke-width="5" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 3. CRT Monitor Outer Shell -->
|
||||||
|
<rect x="112" y="112" width="288" height="228" rx="32" ry="32" fill="#181825" stroke="#cdd6f4" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- 4. Inner Bezel -->
|
||||||
|
<rect x="128" y="132" width="256" height="188" rx="24" ry="24" fill="#313244" stroke="#11111b" stroke-width="6" />
|
||||||
|
|
||||||
|
<!-- 5. CRT Display Screen -->
|
||||||
|
<rect x="140" y="145" width="232" height="162" rx="20" ry="20" fill="url(#screenGlow)" />
|
||||||
|
|
||||||
|
<!-- 6. Screen Content (Clipped to CRT screen bounds) -->
|
||||||
|
<g clip-path="url(#screenClip)">
|
||||||
|
<!-- 4 Thick Scanlines -->
|
||||||
|
<line x1="120" y1="172" x2="392" y2="172" stroke="#11111b" stroke-width="8" opacity="0.25" />
|
||||||
|
<line x1="120" y1="208" x2="392" y2="208" stroke="#11111b" stroke-width="8" opacity="0.25" />
|
||||||
|
<line x1="120" y1="244" x2="392" y2="244" stroke="#11111b" stroke-width="8" opacity="0.25" />
|
||||||
|
<line x1="120" y1="280" x2="392" y2="280" stroke="#11111b" stroke-width="8" opacity="0.25" />
|
||||||
|
|
||||||
|
<!-- Subtle Screen Reflection / Highlight Accent -->
|
||||||
|
<path d="M 145 150 L 260 150 L 145 230 Z" fill="#ffffff" opacity="0.08" />
|
||||||
|
|
||||||
|
<!-- Red Live Indicator Dot with Dark Outer Ring -->
|
||||||
|
<!-- Outer Dark Ring -->
|
||||||
|
<circle cx="182" cy="186" r="22" fill="#11111b" opacity="0.9" />
|
||||||
|
<circle cx="182" cy="186" r="22" stroke="#181825" stroke-width="3" fill="none" />
|
||||||
|
<!-- Red Inner Dot -->
|
||||||
|
<circle cx="182" cy="186" r="14" fill="#f38ba8" />
|
||||||
|
<!-- Inner Highlight on Red Dot -->
|
||||||
|
<circle cx="178" cy="182" r="4" fill="#ffffff" opacity="0.6" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Screen Frame Inner Edge Shadow/Stroke for depth -->
|
||||||
|
<rect x="140" y="145" width="232" height="162" rx="20" ry="20" fill="none" stroke="#11111b" stroke-width="5" opacity="0.4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
hdmiviewer_screenshot_2026-10-17_03-14-50.png
Normal file
BIN
hdmiviewer_screenshot_2026-10-17_03-14-50.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
5
resources.qrc
Normal file
5
resources.qrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/icons">
|
||||||
|
<file alias="hdmi-viewer.svg">assets/hdmi-viewer.svg</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
@@ -422,6 +422,30 @@ void CaptureThread::adjustHardwareControl(quint32 id, const char* settingsKey,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CaptureThread::setHardwareValues(int brightness, int contrast, int saturation)
|
||||||
|
{
|
||||||
|
if (m_fd == -1) return;
|
||||||
|
|
||||||
|
auto set = [&](quint32 id, int value, int restoreValue) {
|
||||||
|
v4l2_queryctrl qc{};
|
||||||
|
qc.id = id;
|
||||||
|
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int target = value >= 0 ? value : restoreValue;
|
||||||
|
if (target < 0) return; // weder Profilwert noch Ausgangswert bekannt
|
||||||
|
|
||||||
|
v4l2_control ctrl{};
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrl.value = qBound(qc.minimum, target, qc.maximum);
|
||||||
|
xioctl(m_fd, VIDIOC_S_CTRL, &ctrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
set(V4L2_CID_BRIGHTNESS, brightness, m_restoreBrightness);
|
||||||
|
set(V4L2_CID_CONTRAST, contrast, m_restoreContrast);
|
||||||
|
set(V4L2_CID_SATURATION, saturation, m_restoreSaturation);
|
||||||
|
}
|
||||||
|
|
||||||
void CaptureThread::adjustHardwareBrightness(int delta)
|
void CaptureThread::adjustHardwareBrightness(int delta)
|
||||||
{
|
{
|
||||||
adjustHardwareControl(V4L2_CID_BRIGHTNESS, "hardware/brightness", "Hardware-Helligkeit", delta);
|
adjustHardwareControl(V4L2_CID_BRIGHTNESS, "hardware/brightness", "Hardware-Helligkeit", delta);
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ public:
|
|||||||
void adjustHardwareBrightness(int delta);
|
void adjustHardwareBrightness(int delta);
|
||||||
void adjustHardwareContrast(int delta);
|
void adjustHardwareContrast(int delta);
|
||||||
void adjustHardwareSaturation(int delta);
|
void adjustHardwareSaturation(int delta);
|
||||||
|
// Setzt die Hardware-Regler absolut (für Profile). -1 = zurück auf den
|
||||||
|
// beim Start gemerkten Ausgangswert des Chips.
|
||||||
|
void setHardwareValues(int brightness, int contrast, int saturation);
|
||||||
|
|
||||||
int captureWidth() const { return m_width; }
|
int captureWidth() const { return m_width; }
|
||||||
int captureHeight() const { return m_height; }
|
int captureHeight() const { return m_height; }
|
||||||
|
|||||||
@@ -55,6 +55,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
|||||||
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
||||||
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
||||||
|
|
||||||
|
// Profile: gespeicherte Kombination aus Glättung/Retro/Schärfen/HW-Reglern
|
||||||
|
m_profileCombo = new QComboBox(this);
|
||||||
|
m_profileSaveBtn = new QPushButton("Speichern …", this);
|
||||||
|
m_profileDeleteBtn = new QPushButton("Löschen", this);
|
||||||
|
m_profileSaveBtn->setAutoDefault(false);
|
||||||
|
m_profileDeleteBtn->setAutoDefault(false);
|
||||||
|
m_profileCombo->setToolTip(
|
||||||
|
"Gespeicherte Bild-Einstellungen (Glättung, Retro, Schärfen,\n"
|
||||||
|
"Hardware-Regler) für eine Quelle, z. B. PlayStation oder PS3.\n"
|
||||||
|
"Im laufenden Betrieb mit [F1]..[F9] oder über das Menü \"Profile\" umschaltbar.");
|
||||||
|
auto* profileRow = new QHBoxLayout;
|
||||||
|
profileRow->addWidget(m_profileCombo, 1);
|
||||||
|
profileRow->addWidget(m_profileSaveBtn);
|
||||||
|
profileRow->addWidget(m_profileDeleteBtn);
|
||||||
|
form->addRow("Profil:", profileRow);
|
||||||
|
|
||||||
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
|
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
|
||||||
m_smoothCheck->setChecked(true);
|
m_smoothCheck->setChecked(true);
|
||||||
m_smoothCheck->setToolTip(
|
m_smoothCheck->setToolTip(
|
||||||
@@ -143,6 +159,24 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
|||||||
connect(btnOk, &QPushButton::clicked, this, &QDialog::accept);
|
connect(btnOk, &QPushButton::clicked, this, &QDialog::accept);
|
||||||
connect(btnClose, &QPushButton::clicked, this, &QDialog::reject);
|
connect(btnClose, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
|
||||||
|
// Profile: Auswahl lädt die Werte in die Felder; manuelle Änderungen
|
||||||
|
// führen die Auswahl nach (passendes Profil oder "kein Profil").
|
||||||
|
populateProfiles(QString());
|
||||||
|
connect(m_profileCombo, QOverload<int>::of(&QComboBox::activated), this, [this](int index) {
|
||||||
|
loadProfileIntoControls(m_profileCombo->itemData(index).toString());
|
||||||
|
});
|
||||||
|
connect(m_profileSaveBtn, &QPushButton::clicked, this, &DeviceDialog::saveProfile);
|
||||||
|
connect(m_profileDeleteBtn, &QPushButton::clicked, this, &DeviceDialog::deleteProfile);
|
||||||
|
|
||||||
|
auto onControlChanged = [this]() { refreshProfileSelection(); };
|
||||||
|
connect(m_smoothCheck, &QCheckBox::toggled, this, onControlChanged);
|
||||||
|
connect(m_retroCheck, &QCheckBox::toggled, this, onControlChanged);
|
||||||
|
connect(m_sharpenSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, onControlChanged);
|
||||||
|
connect(m_hwBrightnessSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||||
|
connect(m_hwContrastSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||||
|
connect(m_hwSaturationSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
|
||||||
|
refreshProfileSelection();
|
||||||
|
|
||||||
auto* main = new QVBoxLayout(this);
|
auto* main = new QVBoxLayout(this);
|
||||||
main->addLayout(form);
|
main->addLayout(form);
|
||||||
main->addSpacing(8);
|
main->addSpacing(8);
|
||||||
@@ -171,6 +205,77 @@ void DeviceDialog::setPreselectedHwBrightness(int value) { m_hwBrightnessSpin->s
|
|||||||
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
|
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
|
||||||
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
|
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
|
||||||
|
|
||||||
|
Profile DeviceDialog::currentProfile() const
|
||||||
|
{
|
||||||
|
Profile p;
|
||||||
|
p.smooth = m_smoothCheck->isChecked();
|
||||||
|
p.retro = m_retroCheck->isChecked();
|
||||||
|
p.sharpen = static_cast<float>(m_sharpenSpin->value());
|
||||||
|
p.hwBrightness = m_hwBrightnessSpin->value();
|
||||||
|
p.hwContrast = m_hwContrastSpin->value();
|
||||||
|
p.hwSaturation = m_hwSaturationSpin->value();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDialog::populateProfiles(const QString& select)
|
||||||
|
{
|
||||||
|
m_profileCombo->clear();
|
||||||
|
m_profileCombo->addItem("– kein Profil –", QString());
|
||||||
|
for (const QString& name : Profiles::names())
|
||||||
|
m_profileCombo->addItem(name, name);
|
||||||
|
selectByData(m_profileCombo, select);
|
||||||
|
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDialog::refreshProfileSelection()
|
||||||
|
{
|
||||||
|
if (m_loadingProfile) return;
|
||||||
|
|
||||||
|
const Profile cur = currentProfile();
|
||||||
|
|
||||||
|
// Aktuell gewähltes Profil bevorzugen, solange es noch passt
|
||||||
|
// (relevant, wenn zwei Profile identische Werte haben).
|
||||||
|
QString match = m_profileCombo->currentData().toString();
|
||||||
|
if (match.isEmpty() || !Profiles::exists(match) || !Profiles::load(match).matches(cur))
|
||||||
|
match = Profiles::findMatching(cur);
|
||||||
|
|
||||||
|
selectByData(m_profileCombo, match);
|
||||||
|
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDialog::loadProfileIntoControls(const QString& name)
|
||||||
|
{
|
||||||
|
m_profileDeleteBtn->setEnabled(!name.isEmpty());
|
||||||
|
if (name.isEmpty() || !Profiles::exists(name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const Profile p = Profiles::load(name);
|
||||||
|
|
||||||
|
m_loadingProfile = true;
|
||||||
|
m_smoothCheck->setChecked(p.smooth);
|
||||||
|
m_retroCheck->setChecked(p.retro);
|
||||||
|
m_sharpenSpin->setValue(p.sharpen);
|
||||||
|
m_hwBrightnessSpin->setValue(p.hwBrightness);
|
||||||
|
m_hwContrastSpin->setValue(p.hwContrast);
|
||||||
|
m_hwSaturationSpin->setValue(p.hwSaturation);
|
||||||
|
m_loadingProfile = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDialog::saveProfile()
|
||||||
|
{
|
||||||
|
QString saved;
|
||||||
|
if (Profiles::saveWithPrompt(this, currentProfile(), &saved))
|
||||||
|
populateProfiles(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDialog::deleteProfile()
|
||||||
|
{
|
||||||
|
const QString name = m_profileCombo->currentData().toString();
|
||||||
|
if (name.isEmpty()) return;
|
||||||
|
if (Profiles::removeWithConfirm(this, name))
|
||||||
|
populateProfiles(QString());
|
||||||
|
}
|
||||||
|
|
||||||
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
|
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < combo->count(); ++i)
|
for (int i = 0; i < combo->count(); ++i)
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Profile.h"
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
|
class QPushButton;
|
||||||
class QCheckBox;
|
class QCheckBox;
|
||||||
class QDoubleSpinBox;
|
class QDoubleSpinBox;
|
||||||
class QSpinBox;
|
class QSpinBox;
|
||||||
@@ -37,10 +40,20 @@ public:
|
|||||||
void setPreselectedHwContrast(int value); // -1 = unverändert
|
void setPreselectedHwContrast(int value); // -1 = unverändert
|
||||||
void setPreselectedHwSaturation(int value); // -1 = unverändert
|
void setPreselectedHwSaturation(int value); // -1 = unverändert
|
||||||
|
|
||||||
|
// Profile: aktueller Stand der Bild-Einstellungen im Dialog, und
|
||||||
|
// Profil-Auswahl passend zu den aktuellen Werten setzen (nach den
|
||||||
|
// setPreselected*-Aufrufen verwenden).
|
||||||
|
Profile currentProfile() const;
|
||||||
|
void refreshProfileSelection();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void populateVideoDevices();
|
void populateVideoDevices();
|
||||||
void populateAudioDevices();
|
void populateAudioDevices();
|
||||||
void selectByData(QComboBox* combo, const QString& data);
|
void selectByData(QComboBox* combo, const QString& data);
|
||||||
|
void populateProfiles(const QString& select);
|
||||||
|
void loadProfileIntoControls(const QString& name);
|
||||||
|
void saveProfile();
|
||||||
|
void deleteProfile();
|
||||||
|
|
||||||
QComboBox* m_videoCombo = nullptr;
|
QComboBox* m_videoCombo = nullptr;
|
||||||
QComboBox* m_audioCombo = nullptr;
|
QComboBox* m_audioCombo = nullptr;
|
||||||
@@ -51,4 +64,8 @@ private:
|
|||||||
QSpinBox* m_hwBrightnessSpin = nullptr;
|
QSpinBox* m_hwBrightnessSpin = nullptr;
|
||||||
QSpinBox* m_hwContrastSpin = nullptr;
|
QSpinBox* m_hwContrastSpin = nullptr;
|
||||||
QSpinBox* m_hwSaturationSpin = nullptr;
|
QSpinBox* m_hwSaturationSpin = nullptr;
|
||||||
|
QComboBox* m_profileCombo = nullptr;
|
||||||
|
QPushButton* m_profileSaveBtn = nullptr;
|
||||||
|
QPushButton* m_profileDeleteBtn = nullptr;
|
||||||
|
bool m_loadingProfile = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
|
||||||
|
#include "Profile.h"
|
||||||
|
|
||||||
MainWindow::MainWindow(const QString& videoDevice,
|
MainWindow::MainWindow(const QString& videoDevice,
|
||||||
const QString& audioDevice,
|
const QString& audioDevice,
|
||||||
int targetFps,
|
int targetFps,
|
||||||
@@ -139,6 +141,12 @@ void MainWindow::setupMenuBar()
|
|||||||
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
|
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
|
||||||
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
|
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
|
||||||
|
|
||||||
|
// Profile werden beim Aufklappen frisch aufgebaut (Häkchen = passt zum
|
||||||
|
// aktuellen Stand), Umschalten auch per F1..F9.
|
||||||
|
m_profileMenu = menuBar()->addMenu("&Profile");
|
||||||
|
connect(m_profileMenu, &QMenu::aboutToShow, this, &MainWindow::rebuildProfileMenu);
|
||||||
|
rebuildProfileMenu();
|
||||||
|
|
||||||
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
||||||
|
|
||||||
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
||||||
@@ -146,6 +154,103 @@ void MainWindow::setupMenuBar()
|
|||||||
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Profile MainWindow::currentProfile() const
|
||||||
|
{
|
||||||
|
// Hardware-Werte stehen laut QSettings immer aktuell (Tasten + Dialog
|
||||||
|
// schreiben sie sofort), die Anzeige-Werte kommen direkt vom Widget.
|
||||||
|
Profile p = Profile::fromSettings();
|
||||||
|
p.smooth = m_video->smoothModeEnabled();
|
||||||
|
p.retro = m_video->retroModeEnabled();
|
||||||
|
p.sharpen = m_video->sharpenAmount();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::rebuildProfileMenu()
|
||||||
|
{
|
||||||
|
if (!m_profileMenu) return;
|
||||||
|
m_profileMenu->clear();
|
||||||
|
|
||||||
|
const QStringList names = Profiles::names();
|
||||||
|
const Profile current = currentProfile();
|
||||||
|
|
||||||
|
for (int i = 0; i < names.size(); ++i) {
|
||||||
|
const QString name = names.at(i);
|
||||||
|
QString label = name;
|
||||||
|
label.replace("&", "&&");
|
||||||
|
if (i < 9)
|
||||||
|
label += QString("\tF%1").arg(i + 1);
|
||||||
|
|
||||||
|
QAction* act = m_profileMenu->addAction(label);
|
||||||
|
act->setCheckable(true);
|
||||||
|
act->setChecked(Profiles::load(name).matches(current));
|
||||||
|
connect(act, &QAction::triggered, this, [this, name]() { applyProfile(name); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
QAction* none = m_profileMenu->addAction("(noch keine Profile gespeichert)");
|
||||||
|
none->setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profileMenu->addSeparator();
|
||||||
|
|
||||||
|
QAction* saveAct = m_profileMenu->addAction("Aktuelle Einstellungen als Profil speichern …");
|
||||||
|
connect(saveAct, &QAction::triggered, this, &MainWindow::saveCurrentAsProfile);
|
||||||
|
|
||||||
|
QMenu* delMenu = m_profileMenu->addMenu("Profil löschen");
|
||||||
|
delMenu->setEnabled(!names.isEmpty());
|
||||||
|
for (const QString& name : names) {
|
||||||
|
QString label = name;
|
||||||
|
label.replace("&", "&&");
|
||||||
|
QAction* act = delMenu->addAction(label);
|
||||||
|
connect(act, &QAction::triggered, this, [this, name]() { deleteProfile(name); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::applyProfile(const QString& name)
|
||||||
|
{
|
||||||
|
if (!Profiles::exists(name)) return;
|
||||||
|
const Profile p = Profiles::load(name);
|
||||||
|
|
||||||
|
m_video->setSmoothModeEnabled(p.smooth);
|
||||||
|
m_video->setRetroModeEnabled(p.retro);
|
||||||
|
|
||||||
|
const float sharpen = qBound(VideoWidget::kSharpenMin, p.sharpen, VideoWidget::kSharpenMax);
|
||||||
|
m_video->setSharpenAmount(sharpen);
|
||||||
|
if (sharpen > 0.0f)
|
||||||
|
m_lastSharpenAmount = sharpen;
|
||||||
|
|
||||||
|
if (m_smoothAction) m_smoothAction->setChecked(p.smooth);
|
||||||
|
if (m_retroAction) m_retroAction->setChecked(p.retro);
|
||||||
|
if (m_sharpenAction) m_sharpenAction->setChecked(sharpen > 0.0f);
|
||||||
|
|
||||||
|
// Als aktuellen Stand persistieren (nächster Start übernimmt ihn) ...
|
||||||
|
p.applyToSettings();
|
||||||
|
|
||||||
|
// ... und die Hardware-Regler am Grabber sofort umstellen.
|
||||||
|
if (m_capture)
|
||||||
|
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
|
||||||
|
|
||||||
|
statusBar()->showMessage("Profil geladen: " + name, 3000);
|
||||||
|
showOverlayMessage("Profil: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::saveCurrentAsProfile()
|
||||||
|
{
|
||||||
|
QString saved;
|
||||||
|
if (Profiles::saveWithPrompt(this, currentProfile(), &saved)) {
|
||||||
|
statusBar()->showMessage("Profil gespeichert: " + saved, 3000);
|
||||||
|
showOverlayMessage("Profil gespeichert: " + saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::deleteProfile(const QString& name)
|
||||||
|
{
|
||||||
|
if (Profiles::removeWithConfirm(this, name))
|
||||||
|
statusBar()->showMessage("Profil gelöscht: " + name, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::showOverlayMessage(const QString& text)
|
void MainWindow::showOverlayMessage(const QString& text)
|
||||||
{
|
{
|
||||||
if (!m_overlayLabel) return;
|
if (!m_overlayLabel) return;
|
||||||
@@ -249,7 +354,19 @@ void MainWindow::toggleRaw()
|
|||||||
const bool enabled = !m_video->rawEnabled();
|
const bool enabled = !m_video->rawEnabled();
|
||||||
m_video->setRawEnabled(enabled);
|
m_video->setRawEnabled(enabled);
|
||||||
|
|
||||||
const QString msg = enabled ? "Roh-Modus: an (alle Effekte umgangen)" : "Roh-Modus: aus";
|
// Auch die Hardware-Regler am Grabber-Chip umgehen: An = zurück auf die
|
||||||
|
// beim Start gemerkten Ausgangswerte des Chips (-1), Aus = gespeicherte
|
||||||
|
// Nutzerwerte (QSettings bzw. aktives Profil) wieder setzen.
|
||||||
|
if (m_capture) {
|
||||||
|
if (enabled) {
|
||||||
|
m_capture->setHardwareValues(-1, -1, -1);
|
||||||
|
} else {
|
||||||
|
const Profile p = Profile::fromSettings();
|
||||||
|
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString msg = enabled ? "Roh-Modus: an (alle Filter und HW-Regler aus)" : "Roh-Modus: aus";
|
||||||
statusBar()->showMessage(msg, 2000);
|
statusBar()->showMessage(msg, 2000);
|
||||||
showOverlayMessage(msg);
|
showOverlayMessage(msg);
|
||||||
}
|
}
|
||||||
@@ -287,7 +404,7 @@ void MainWindow::showAbout()
|
|||||||
{
|
{
|
||||||
auto* dlg = new QDialog(this);
|
auto* dlg = new QDialog(this);
|
||||||
dlg->setWindowTitle("Über HDMI Viewer");
|
dlg->setWindowTitle("Über HDMI Viewer");
|
||||||
dlg->setFixedSize(460, 470);
|
dlg->setFixedSize(460, 490);
|
||||||
dlg->setModal(true);
|
dlg->setModal(true);
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(dlg);
|
auto* root = new QVBoxLayout(dlg);
|
||||||
@@ -336,7 +453,7 @@ void MainWindow::showAbout()
|
|||||||
grid->addWidget(v, row, 1);
|
grid->addWidget(v, row, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
addRow(0, "Version", "1.0");
|
addRow(0, "Version", "1.1.0");
|
||||||
addRow(1, "Entwickler", "Dany Thinnes");
|
addRow(1, "Entwickler", "Dany Thinnes");
|
||||||
addRow(2, "Projekt", "Projekt Hirnfrei");
|
addRow(2, "Projekt", "Projekt Hirnfrei");
|
||||||
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
||||||
@@ -360,6 +477,7 @@ void MainWindow::showAbout()
|
|||||||
"<b>1</b>/<b>2</b> HW-Kontrast "
|
"<b>1</b>/<b>2</b> HW-Kontrast "
|
||||||
"<b>3</b>/<b>4</b> HW-Sättigung "
|
"<b>3</b>/<b>4</b> HW-Sättigung "
|
||||||
"<b>R</b> Roh-Diagnose<br>"
|
"<b>R</b> Roh-Diagnose<br>"
|
||||||
|
"<b>F1</b>–<b>F9</b> Profile<br>"
|
||||||
"<b>V</b> Vergleich "
|
"<b>V</b> Vergleich "
|
||||||
"<b>P</b> Screenshot "
|
"<b>P</b> Screenshot "
|
||||||
"<b>D</b> Frame dumpen "
|
"<b>D</b> Frame dumpen "
|
||||||
@@ -404,6 +522,17 @@ void MainWindow::stopAll()
|
|||||||
|
|
||||||
void MainWindow::keyPressEvent(QKeyEvent* event)
|
void MainWindow::keyPressEvent(QKeyEvent* event)
|
||||||
{
|
{
|
||||||
|
// F1..F9: Profil 1..9 (Reihenfolge wie im Profile-Menü, alphabetisch)
|
||||||
|
if (event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F9) {
|
||||||
|
const QStringList names = Profiles::names();
|
||||||
|
const int idx = event->key() - Qt::Key_F1;
|
||||||
|
if (idx < names.size())
|
||||||
|
applyProfile(names.at(idx));
|
||||||
|
else
|
||||||
|
showOverlayMessage(QString("Kein Profil auf F%1").arg(idx + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event->key()) {
|
switch (event->key()) {
|
||||||
case Qt::Key_F:
|
case Qt::Key_F:
|
||||||
toggleFullscreen();
|
toggleFullscreen();
|
||||||
@@ -494,7 +623,7 @@ void MainWindow::updateStatusBar()
|
|||||||
info = QString("%1×%2 @ %3fps %4 ")
|
info = QString("%1×%2 @ %3fps %4 ")
|
||||||
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
|
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
|
||||||
|
|
||||||
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
|
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [F1-F9] Profile [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
|
||||||
.arg(m_videoDevice,
|
.arg(m_videoDevice,
|
||||||
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Profile.h"
|
||||||
|
|
||||||
class VideoWidget;
|
class VideoWidget;
|
||||||
class CaptureThread;
|
class CaptureThread;
|
||||||
class AudioThread;
|
class AudioThread;
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QAction;
|
class QAction;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QMenu;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
@@ -57,6 +60,14 @@ private:
|
|||||||
void applySharpen(float value);
|
void applySharpen(float value);
|
||||||
void showOverlayMessage(const QString& text);
|
void showOverlayMessage(const QString& text);
|
||||||
|
|
||||||
|
// Profile
|
||||||
|
Profile currentProfile() const;
|
||||||
|
void rebuildProfileMenu();
|
||||||
|
void applyProfile(const QString& name);
|
||||||
|
void saveCurrentAsProfile();
|
||||||
|
void deleteProfile(const QString& name);
|
||||||
|
|
||||||
|
QMenu* m_profileMenu = nullptr;
|
||||||
VideoWidget* m_video = nullptr;
|
VideoWidget* m_video = nullptr;
|
||||||
CaptureThread* m_capture = nullptr;
|
CaptureThread* m_capture = nullptr;
|
||||||
AudioThread* m_audio = nullptr;
|
AudioThread* m_audio = nullptr;
|
||||||
|
|||||||
168
src/Profile.cpp
Normal file
168
src/Profile.cpp
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#include "Profile.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool Profile::matches(const Profile& o) const
|
||||||
|
{
|
||||||
|
return smooth == o.smooth
|
||||||
|
&& retro == o.retro
|
||||||
|
&& qAbs(sharpen - o.sharpen) < 0.05f // Spinbox hat 1 Nachkommastelle
|
||||||
|
&& hwBrightness == o.hwBrightness
|
||||||
|
&& hwContrast == o.hwContrast
|
||||||
|
&& hwSaturation == o.hwSaturation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile Profile::fromSettings()
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
Profile p;
|
||||||
|
p.smooth = s.value("display/smooth", true).toBool();
|
||||||
|
p.retro = s.value("display/retro", false).toBool();
|
||||||
|
p.sharpen = s.value("display/sharpen", 0.0).toFloat();
|
||||||
|
p.hwBrightness = s.contains("hardware/brightness") ? s.value("hardware/brightness").toInt() : -1;
|
||||||
|
p.hwContrast = s.contains("hardware/contrast") ? s.value("hardware/contrast").toInt() : -1;
|
||||||
|
p.hwSaturation = s.contains("hardware/saturation") ? s.value("hardware/saturation").toInt() : -1;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Profile::applyToSettings() const
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
s.setValue("display/smooth", smooth);
|
||||||
|
s.setValue("display/retro", retro);
|
||||||
|
s.setValue("display/sharpen", static_cast<double>(sharpen));
|
||||||
|
if (sharpen > 0.0f)
|
||||||
|
s.setValue("display/sharpen_last", static_cast<double>(sharpen));
|
||||||
|
|
||||||
|
auto setOrRemove = [&](const char* key, int value) {
|
||||||
|
if (value >= 0) s.setValue(key, value);
|
||||||
|
else s.remove(key);
|
||||||
|
};
|
||||||
|
setOrRemove("hardware/brightness", hwBrightness);
|
||||||
|
setOrRemove("hardware/contrast", hwContrast);
|
||||||
|
setOrRemove("hardware/saturation", hwSaturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Profiles ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace Profiles
|
||||||
|
{
|
||||||
|
|
||||||
|
QStringList names()
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup("profiles");
|
||||||
|
QStringList list = s.childGroups();
|
||||||
|
s.endGroup();
|
||||||
|
list.sort(Qt::CaseInsensitive);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool exists(const QString& name)
|
||||||
|
{
|
||||||
|
return isValidName(name) && names().contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile load(const QString& name)
|
||||||
|
{
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup("profiles/" + name);
|
||||||
|
Profile p;
|
||||||
|
p.smooth = s.value("smooth", true).toBool();
|
||||||
|
p.retro = s.value("retro", false).toBool();
|
||||||
|
p.sharpen = s.value("sharpen", 0.0).toFloat();
|
||||||
|
p.hwBrightness = s.value("hwBrightness", -1).toInt();
|
||||||
|
p.hwContrast = s.value("hwContrast", -1).toInt();
|
||||||
|
p.hwSaturation = s.value("hwSaturation", -1).toInt();
|
||||||
|
s.endGroup();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save(const QString& name, const Profile& p)
|
||||||
|
{
|
||||||
|
if (!isValidName(name)) return;
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup("profiles/" + name);
|
||||||
|
s.setValue("smooth", p.smooth);
|
||||||
|
s.setValue("retro", p.retro);
|
||||||
|
s.setValue("sharpen", static_cast<double>(p.sharpen));
|
||||||
|
s.setValue("hwBrightness", p.hwBrightness);
|
||||||
|
s.setValue("hwContrast", p.hwContrast);
|
||||||
|
s.setValue("hwSaturation", p.hwSaturation);
|
||||||
|
s.endGroup();
|
||||||
|
s.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool remove(const QString& name)
|
||||||
|
{
|
||||||
|
if (!exists(name)) return false;
|
||||||
|
QSettings s;
|
||||||
|
s.remove("profiles/" + name);
|
||||||
|
s.sync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValidName(const QString& name)
|
||||||
|
{
|
||||||
|
// '/' und '\' wären für QSettings Gruppentrenner
|
||||||
|
return !name.trimmed().isEmpty()
|
||||||
|
&& name == name.trimmed()
|
||||||
|
&& !name.contains('/')
|
||||||
|
&& !name.contains('\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
QString findMatching(const Profile& profile)
|
||||||
|
{
|
||||||
|
for (const QString& n : names())
|
||||||
|
if (load(n).matches(profile))
|
||||||
|
return n;
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName)
|
||||||
|
{
|
||||||
|
const QStringList existing = names();
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
const QString input = QInputDialog::getItem(
|
||||||
|
parent, "Profil speichern",
|
||||||
|
"Name des Profils (neuen Namen eintippen\noder vorhandenes zum Überschreiben wählen):",
|
||||||
|
existing, 0, true, &ok);
|
||||||
|
if (!ok) return false;
|
||||||
|
|
||||||
|
const QString name = input.trimmed();
|
||||||
|
if (!isValidName(name)) {
|
||||||
|
QMessageBox::warning(parent, "Profil speichern",
|
||||||
|
"Ungültiger Name. Der Name darf nicht leer sein und weder '/' noch '\\' enthalten.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.contains(name)) {
|
||||||
|
const auto answer = QMessageBox::question(
|
||||||
|
parent, "Profil überschreiben",
|
||||||
|
QString("Das Profil „%1“ existiert bereits. Überschreiben?").arg(name),
|
||||||
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||||
|
if (answer != QMessageBox::Yes) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(name, profile);
|
||||||
|
if (savedName) *savedName = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removeWithConfirm(QWidget* parent, const QString& name)
|
||||||
|
{
|
||||||
|
const auto answer = QMessageBox::question(
|
||||||
|
parent, "Profil löschen",
|
||||||
|
QString("Profil „%1“ wirklich löschen?").arg(name),
|
||||||
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||||
|
if (answer != QMessageBox::Yes) return false;
|
||||||
|
return remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Profiles
|
||||||
49
src/Profile.h
Normal file
49
src/Profile.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QWidget;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
// Ein Profil bündelt alle Bildeinstellungen, die sich pro Konsole/Quelle
|
||||||
|
// unterscheiden (z. B. PlayStation: aufhellen + Filter, PS3: alles neutral).
|
||||||
|
// Hardware-Werte: -1 = "unverändert" (Regler am Grabber-Chip nicht anfassen
|
||||||
|
// bzw. auf den Ausgangswert des Chips zurücksetzen).
|
||||||
|
struct Profile
|
||||||
|
{
|
||||||
|
bool smooth = true;
|
||||||
|
bool retro = false;
|
||||||
|
float sharpen = 0.0f; // 0 = aus
|
||||||
|
int hwBrightness = -1;
|
||||||
|
int hwContrast = -1;
|
||||||
|
int hwSaturation = -1;
|
||||||
|
|
||||||
|
bool matches(const Profile& other) const;
|
||||||
|
|
||||||
|
// Aktueller Stand laut QSettings (display/* und hardware/*).
|
||||||
|
static Profile fromSettings();
|
||||||
|
|
||||||
|
// Schreibt das Profil als aktuellen Stand nach display/* und hardware/*.
|
||||||
|
void applyToSettings() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Profilverwaltung (gespeichert in QSettings unter profiles/<Name>/...).
|
||||||
|
namespace Profiles
|
||||||
|
{
|
||||||
|
QStringList names(); // alphabetisch sortiert
|
||||||
|
bool exists(const QString& name);
|
||||||
|
Profile load(const QString& name);
|
||||||
|
void save(const QString& name, const Profile& profile);
|
||||||
|
bool remove(const QString& name);
|
||||||
|
bool isValidName(const QString& name);
|
||||||
|
QString findMatching(const Profile& profile); // "" falls keins passt
|
||||||
|
|
||||||
|
// Dialog: Namen eingeben bzw. vorhandenes Profil wählen (Überschreiben
|
||||||
|
// wird nachgefragt) und speichern. true = gespeichert.
|
||||||
|
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName = nullptr);
|
||||||
|
|
||||||
|
// Löschen nach Rückfrage. true = gelöscht.
|
||||||
|
bool removeWithConfirm(QWidget* parent, const QString& name);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QOpenGLTexture>
|
#include <QOpenGLTexture>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ uniform float u_sharpen;
|
|||||||
uniform float u_gamma;
|
uniform float u_gamma;
|
||||||
uniform int u_raw;
|
uniform int u_raw;
|
||||||
uniform int u_compare;
|
uniform int u_compare;
|
||||||
|
uniform int u_debug;
|
||||||
|
|
||||||
vec3 rgb2ycbcr(vec3 c)
|
vec3 rgb2ycbcr(vec3 c)
|
||||||
{
|
{
|
||||||
@@ -159,6 +161,12 @@ void main()
|
|||||||
{
|
{
|
||||||
vec2 uv = v_tex;
|
vec2 uv = v_tex;
|
||||||
|
|
||||||
|
// Diagnose (nur per HDMIVIEWER_DEBUG=shaderwhite): Shader gibt reines Weiss aus.
|
||||||
|
if (u_debug == 1) {
|
||||||
|
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
|
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
|
||||||
if (u_raw == 1) {
|
if (u_raw == 1) {
|
||||||
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
|
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
|
||||||
@@ -338,6 +346,14 @@ void VideoWidget::initializeGL()
|
|||||||
initializeOpenGLFunctions();
|
initializeOpenGLFunctions();
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Diagnose-Ausgabe einmalig im Terminal: welche GL-Umgebung/Pixelformat aktiv ist.
|
||||||
|
qDebug().noquote() << "GL_RENDERER:" << reinterpret_cast<const char*>(glGetString(GL_RENDERER))
|
||||||
|
<< " GL_VERSION:" << reinterpret_cast<const char*>(glGetString(GL_VERSION));
|
||||||
|
qDebug().noquote() << QString("Surface: R%1 G%2 B%3 A%4 samples=%5")
|
||||||
|
.arg(format().redBufferSize()).arg(format().greenBufferSize())
|
||||||
|
.arg(format().blueBufferSize()).arg(format().alphaBufferSize())
|
||||||
|
.arg(format().samples());
|
||||||
|
|
||||||
m_program = new QOpenGLShaderProgram(this);
|
m_program = new QOpenGLShaderProgram(this);
|
||||||
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
|
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
|
||||||
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
|
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
|
||||||
@@ -403,11 +419,81 @@ void VideoWidget::paintGL()
|
|||||||
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Diagnose-Schalter per Umgebungsvariable (im Normalbetrieb ohne Wirkung):
|
||||||
|
// HDMIVIEWER_DEBUG=clearwhite -> Framebuffer nur mit Weiss loeschen, kein Shader
|
||||||
|
// HDMIVIEWER_DEBUG=shaderwhite -> Shader wird benutzt, gibt aber reines Weiss aus
|
||||||
|
static const QByteArray dbg = qgetenv("HDMIVIEWER_DEBUG");
|
||||||
|
if (dbg == "clearwhite") {
|
||||||
|
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_hasFrame || !m_texture) {
|
if (!m_hasFrame || !m_texture) {
|
||||||
drawNoSignal();
|
drawNoSignal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnose (HDMIVIEWER_DEBUG=state oder =reset): GL-Zustand direkt VOR dem Zeichnen
|
||||||
|
// einmalig ins Terminal schreiben (nach einem moeglichen QPainter in drawNoSignal).
|
||||||
|
if (dbg == "state" || dbg == "reset") {
|
||||||
|
static bool dumped = false;
|
||||||
|
if (!dumped) {
|
||||||
|
dumped = true;
|
||||||
|
auto en = [this](GLenum cap) { return glIsEnabled(cap) ? 1 : 0; };
|
||||||
|
GLint bsr = 0, bdr = 0, bsa = 0, bda = 0, beq = 0, fbo = 0, enc = 0, ctype = 0;
|
||||||
|
GLfloat bc[4] = {0, 0, 0, 0};
|
||||||
|
GLboolean cm[4] = {0, 0, 0, 0};
|
||||||
|
glGetIntegerv(0x80C9, &bsr); // GL_BLEND_SRC_RGB
|
||||||
|
glGetIntegerv(0x80C8, &bdr); // GL_BLEND_DST_RGB
|
||||||
|
glGetIntegerv(0x80CB, &bsa); // GL_BLEND_SRC_ALPHA
|
||||||
|
glGetIntegerv(0x80CA, &bda); // GL_BLEND_DST_ALPHA
|
||||||
|
glGetIntegerv(0x8009, &beq); // GL_BLEND_EQUATION_RGB
|
||||||
|
glGetIntegerv(0x8CA6, &fbo); // GL_DRAW_FRAMEBUFFER_BINDING
|
||||||
|
glGetFloatv(0x8005, bc); // GL_BLEND_COLOR
|
||||||
|
glGetBooleanv(0x0C23, cm); // GL_COLOR_WRITEMASK
|
||||||
|
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8210, &enc); // COLOR_ENCODING
|
||||||
|
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8211, &ctype); // COMPONENT_TYPE
|
||||||
|
const QStringList st = {
|
||||||
|
QString("BLEND=%1").arg(en(0x0BE2)),
|
||||||
|
QString("blendRGB src=0x%1 dst=0x%2").arg(bsr, 0, 16).arg(bdr, 0, 16),
|
||||||
|
QString("blendA src=0x%1 dst=0x%2").arg(bsa, 0, 16).arg(bda, 0, 16),
|
||||||
|
QString("eq=0x%1").arg(beq, 0, 16),
|
||||||
|
QString("blendColor=%1,%2,%3,%4").arg(bc[0]).arg(bc[1]).arg(bc[2]).arg(bc[3]),
|
||||||
|
QString("colorMask=%1%2%3%4").arg(int(cm[0])).arg(int(cm[1])).arg(int(cm[2])).arg(int(cm[3])),
|
||||||
|
QString("FB_SRGB=%1").arg(en(0x8DB9)),
|
||||||
|
QString("LOGIC_OP=%1").arg(en(0x0BF2)),
|
||||||
|
QString("A2C=%1").arg(en(0x809E)),
|
||||||
|
QString("DITHER=%1").arg(en(0x0BD0)),
|
||||||
|
QString("MULTISAMPLE=%1").arg(en(0x809D)),
|
||||||
|
QString("SCISSOR=%1").arg(en(0x0C11)),
|
||||||
|
QString("DEPTH=%1").arg(en(0x0B71)),
|
||||||
|
QString("STENCIL=%1").arg(en(0x0B90)),
|
||||||
|
QString("CULL=%1").arg(en(0x0B44)),
|
||||||
|
QString("drawFBO=%1").arg(fbo),
|
||||||
|
QString("attachEncoding=0x%1 (0x8c40=sRGB, 0x2601=linear)").arg(enc, 0, 16),
|
||||||
|
QString("attachType=0x%1 (0x8c17=UNORM, 0x8f9c=SNORM, 0x1406=FLOAT)").arg(ctype, 0, 16)
|
||||||
|
};
|
||||||
|
qDebug().noquote() << "GL-State vor dem Zeichnen:" << st.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fester Zustands-Reset vor dem Zeichnen: drawNoSignal() nutzt QPainter, der GL-Zustand
|
||||||
|
// (u. a. Blending) zuruecklaesst. Ohne diesen Reset war das Bild live/im Screenshot
|
||||||
|
// exakt auf die Haelfte abgedunkelt (Weiss 255 -> 128), auch im Roh-Modus.
|
||||||
|
{
|
||||||
|
glDisable(0x0BE2); // GL_BLEND
|
||||||
|
glDisable(0x0B71); // GL_DEPTH_TEST
|
||||||
|
glDisable(0x0B90); // GL_STENCIL_TEST
|
||||||
|
glDisable(0x0C11); // GL_SCISSOR_TEST
|
||||||
|
glDisable(0x0B44); // GL_CULL_FACE
|
||||||
|
glDisable(0x8DB9); // GL_FRAMEBUFFER_SRGB
|
||||||
|
glDisable(0x0BF2); // GL_COLOR_LOGIC_OP
|
||||||
|
glDisable(0x809E); // GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||||
|
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
const float viewW = static_cast<float>(width());
|
const float viewW = static_cast<float>(width());
|
||||||
const float viewH = static_cast<float>(height());
|
const float viewH = static_cast<float>(height());
|
||||||
const float imgW = static_cast<float>(m_imageSize.width());
|
const float imgW = static_cast<float>(m_imageSize.width());
|
||||||
@@ -441,6 +527,7 @@ void VideoWidget::paintGL()
|
|||||||
m_program->setUniformValue("u_gamma", m_brightness);
|
m_program->setUniformValue("u_gamma", m_brightness);
|
||||||
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
|
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
|
||||||
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
|
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
|
||||||
|
m_program->setUniformValue("u_debug", dbg == "shaderwhite" ? 1 : 0);
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
|
#include <QIcon>
|
||||||
|
|
||||||
#include "DeviceDialog.h"
|
#include "DeviceDialog.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
@@ -18,9 +19,13 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setApplicationName("HDMIViewer");
|
app.setApplicationName("HDMIViewer");
|
||||||
app.setApplicationVersion("1.0");
|
app.setApplicationVersion("1.1.0");
|
||||||
app.setOrganizationName("hdmi-viewer");
|
app.setOrganizationName("hdmi-viewer");
|
||||||
|
|
||||||
|
// Fenster-/Taskleisten-Icon: installiertes Theme-Icon bevorzugen, sonst das
|
||||||
|
// eingebettete SVG (z. B. beim Start direkt aus dem build-Ordner).
|
||||||
|
app.setWindowIcon(QIcon::fromTheme("hdmi-viewer", QIcon(":/icons/hdmi-viewer.svg")));
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -34,6 +39,7 @@ int main(int argc, char* argv[])
|
|||||||
dlg.setPreselectedHwBrightness(settings.value("hardware/brightness", -1).toInt());
|
dlg.setPreselectedHwBrightness(settings.value("hardware/brightness", -1).toInt());
|
||||||
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
|
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
|
||||||
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
|
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
|
||||||
|
dlg.refreshProfileSelection(); // zeigt das Profil, das zu den Werten passt
|
||||||
|
|
||||||
if (dlg.exec() != QDialog::Accepted)
|
if (dlg.exec() != QDialog::Accepted)
|
||||||
break;
|
break;
|
||||||
|
|||||||
262
tools/colormatch.py
Normal file
262
tools/colormatch.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
colormatch.py - Farbabgleich zwischen einem Referenzbild (z. B. UARTScope)
|
||||||
|
und einem Testbild (z. B. HDMIViewer), das zu dunkel/anders erscheint.
|
||||||
|
|
||||||
|
Es wird KEINE Pixel-fuer-Pixel-Gleichheit verlangt (die Bilder duerfen
|
||||||
|
unterschiedliche Aufloesung und leicht unterschiedlichen Inhalt haben, z. B.
|
||||||
|
eine andere Animationsphase). Stattdessen werden pro Farbkanal die
|
||||||
|
Helligkeitsverteilungen (Quantile) beider Bilder zugeordnet und daraus die
|
||||||
|
Korrektur bestimmt, die das Testbild an die Referenz angleicht:
|
||||||
|
|
||||||
|
ref ~= T(test)
|
||||||
|
|
||||||
|
Gefittet werden drei Modelle pro Kanal (Werte normiert auf 0..1):
|
||||||
|
gain : out = g * in
|
||||||
|
linear : out = g * in + o
|
||||||
|
gamma : out = g * in ** gamma
|
||||||
|
|
||||||
|
Ausgabe: Werte, Fehler (RMS in 0..255) und fertige GLSL-Zeilen fuer
|
||||||
|
VideoWidget.cpp. Optional LUT als CSV und das korrigierte Testbild.
|
||||||
|
|
||||||
|
WICHTIG: Fuer eine sinnvolle Aussage muessen beide Bilder auf demselben Weg
|
||||||
|
entstanden sein, z. B. beide per Desktop-Screenshot (Spectacle o. ae.) des
|
||||||
|
LAUFENDEN Programms, jeweils auf das Videobild zugeschnitten. Ein per P
|
||||||
|
gespeicherter HDMIViewer-Screenshot enthaelt nur den Framebuffer und zeigt
|
||||||
|
Unterschiede, die erst bei der Anzeige am Bildschirm entstehen, nicht.
|
||||||
|
|
||||||
|
Ist die Verarbeitung nicht linear (Kontrast, Studio-Range ...), liefert der
|
||||||
|
Fit nur eine Naeherung (Fehler deutlich ueber ~3). Dann hilft --palette: Die
|
||||||
|
haeufigsten Flaechenfarben zeigen direkt, was mit Weiss/Rot/... passiert.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
colormatch.py REFERENZ.png TEST.png
|
||||||
|
colormatch.py ref.png test.png --palette
|
||||||
|
colormatch.py ref.png test.png --crop-ref 100,80,1280,720 --crop-test 0,0,1280,672
|
||||||
|
colormatch.py ref.png test.png --lut lut.csv --save-corrected korrigiert.png
|
||||||
|
|
||||||
|
Benoetigt: python3, numpy, Pillow (Arch: pacman -S python-numpy python-pillow)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
CHANNELS = ("R", "G", "B")
|
||||||
|
N_QUANT = 4096 # Anzahl der Quantil-Stuetzstellen pro Kanal
|
||||||
|
|
||||||
|
|
||||||
|
def parse_crop(text):
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
x, y, w, h = (int(v) for v in text.split(","))
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError("Format: x,y,breite,hoehe")
|
||||||
|
return x, y, w, h
|
||||||
|
|
||||||
|
|
||||||
|
def load(path, crop, auto, thr):
|
||||||
|
arr = np.asarray(Image.open(path).convert("RGB"))
|
||||||
|
if crop:
|
||||||
|
x, y, w, h = crop
|
||||||
|
arr = arr[y:y + h, x:x + w]
|
||||||
|
if auto:
|
||||||
|
# Schwarze Letterbox-/Pillarbox-Raender abschneiden
|
||||||
|
mask = arr.max(axis=2) > thr
|
||||||
|
ys = np.where(mask.any(axis=1))[0]
|
||||||
|
xs = np.where(mask.any(axis=0))[0]
|
||||||
|
if len(ys) and len(xs):
|
||||||
|
arr = arr[ys[0]:ys[-1] + 1, xs[0]:xs[-1] + 1]
|
||||||
|
if arr.size == 0:
|
||||||
|
sys.exit(f"Leerer Ausschnitt bei {path}")
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
def quantile_pairs(test_ch, ref_ch):
|
||||||
|
"""Ordnet Quantile des Testkanals denen des Referenzkanals zu.
|
||||||
|
Rueckgabe: (t, r, w) - eindeutige Wertepaare (0..1) mit Gewicht."""
|
||||||
|
ts = np.sort(test_ch.ravel())
|
||||||
|
rs = np.sort(ref_ch.ravel())
|
||||||
|
p = (np.arange(N_QUANT) + 0.5) / N_QUANT
|
||||||
|
t = ts[(p * (len(ts) - 1)).astype(np.int64)].astype(np.float64) / 255.0
|
||||||
|
r = rs[(p * (len(rs) - 1)).astype(np.int64)].astype(np.float64) / 255.0
|
||||||
|
pairs = np.stack([t, r], axis=1)
|
||||||
|
uniq, cnt = np.unique(pairs, axis=0, return_counts=True)
|
||||||
|
return uniq[:, 0], uniq[:, 1], cnt.astype(np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def fit_gain(t, r, w):
|
||||||
|
den = np.sum(w * t * t)
|
||||||
|
g = np.sum(w * t * r) / den if den > 0 else 1.0
|
||||||
|
return {"g": g}
|
||||||
|
|
||||||
|
|
||||||
|
def fit_linear(t, r, w):
|
||||||
|
sw = np.sqrt(w)
|
||||||
|
a = np.stack([t, np.ones_like(t)], axis=1) * sw[:, None]
|
||||||
|
sol, *_ = np.linalg.lstsq(a, r * sw, rcond=None)
|
||||||
|
return {"g": sol[0], "o": sol[1]}
|
||||||
|
|
||||||
|
|
||||||
|
def fit_gamma(t, r, w):
|
||||||
|
m = (t > 1.0 / 255.0) & (r > 1.0 / 255.0)
|
||||||
|
if m.sum() < 2:
|
||||||
|
return {"g": 1.0, "gamma": 1.0}
|
||||||
|
sw = np.sqrt(w[m])
|
||||||
|
a = np.stack([np.ones(m.sum()), np.log(t[m])], axis=1) * sw[:, None]
|
||||||
|
sol, *_ = np.linalg.lstsq(a, np.log(r[m]) * sw, rcond=None)
|
||||||
|
return {"g": float(np.exp(sol[0])), "gamma": float(sol[1])}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_model(name, p, x):
|
||||||
|
if name == "gain":
|
||||||
|
y = p["g"] * x
|
||||||
|
elif name == "linear":
|
||||||
|
y = p["g"] * x + p["o"]
|
||||||
|
else:
|
||||||
|
y = p["g"] * np.power(np.clip(x, 0.0, 1.0), p["gamma"])
|
||||||
|
return np.clip(y, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def rms255(name, p, t, r, w):
|
||||||
|
d = (apply_model(name, p, t) - r) * 255.0
|
||||||
|
return float(np.sqrt(np.sum(w * d * d) / np.sum(w)))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("ref", help="Referenzbild (so soll es aussehen, z. B. UARTScope)")
|
||||||
|
ap.add_argument("test", help="Testbild (so sieht es aus, z. B. HDMIViewer)")
|
||||||
|
ap.add_argument("--crop-ref", type=parse_crop, help="Ausschnitt x,y,b,h im Referenzbild")
|
||||||
|
ap.add_argument("--crop-test", type=parse_crop, help="Ausschnitt x,y,b,h im Testbild")
|
||||||
|
ap.add_argument("--no-autocrop", action="store_true",
|
||||||
|
help="schwarze Raender NICHT automatisch abschneiden")
|
||||||
|
ap.add_argument("--black-threshold", type=int, default=8,
|
||||||
|
help="Schwellwert fuer die Randerkennung (Standard 8)")
|
||||||
|
ap.add_argument("--palette", action="store_true",
|
||||||
|
help="zusaetzlich die haeufigsten Flaechenfarben beider Bilder ausgeben")
|
||||||
|
ap.add_argument("--lut", metavar="CSV", help="256-Werte-LUT pro Kanal als CSV speichern")
|
||||||
|
ap.add_argument("--save-corrected", metavar="PNG",
|
||||||
|
help="Testbild mit der besten Korrektur speichern")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
auto = not args.no_autocrop
|
||||||
|
ref = load(args.ref, args.crop_ref, auto, args.black_threshold)
|
||||||
|
test = load(args.test, args.crop_test, auto, args.black_threshold)
|
||||||
|
|
||||||
|
print(f"Referenz: {ref.shape[1]}x{ref.shape[0]} Test: {test.shape[1]}x{test.shape[0]}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if args.palette:
|
||||||
|
# Haeufigste Farben (ohne fast schwarze Pixel). Bei Bildern mit flachen
|
||||||
|
# Farbflaechen (Amiga, Konsolen) zeigt das am direktesten, wie die
|
||||||
|
# Helligkeit veraendert wird, z. B. Weiss 255 -> 128 = Faktor 0,5.
|
||||||
|
for label, img in (("Referenz", ref), ("Test", test)):
|
||||||
|
px = img.reshape(-1, 3)
|
||||||
|
px = px[px.max(axis=1) > 8]
|
||||||
|
u, c = np.unique(px, axis=0, return_counts=True)
|
||||||
|
print(f"Haeufigste Farben {label}:")
|
||||||
|
for i in np.argsort(-c)[:6]:
|
||||||
|
print(f" ({u[i][0]:3d},{u[i][1]:3d},{u[i][2]:3d}) {c[i]} Pixel")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Mittelwerte (0..255) R G B")
|
||||||
|
mr = ref.reshape(-1, 3).mean(axis=0)
|
||||||
|
mt = test.reshape(-1, 3).mean(axis=0)
|
||||||
|
print(" Referenz " + "".join(f"{v:9.2f}" for v in mr))
|
||||||
|
print(" Test " + "".join(f"{v:9.2f}" for v in mt))
|
||||||
|
print(" Verhaeltnis Ref/Test" + "".join(f"{(a / b if b > 0 else float('nan')):9.3f}"
|
||||||
|
for a, b in zip(mr, mt)))
|
||||||
|
print()
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
luts = []
|
||||||
|
for ci, cname in enumerate(CHANNELS):
|
||||||
|
t, r, w = quantile_pairs(test[..., ci], ref[..., ci])
|
||||||
|
models = {
|
||||||
|
"gain": fit_gain(t, r, w),
|
||||||
|
"linear": fit_linear(t, r, w),
|
||||||
|
"gamma": fit_gamma(t, r, w),
|
||||||
|
}
|
||||||
|
errs = {n: rms255(n, p, t, r, w) for n, p in models.items()}
|
||||||
|
results[cname] = (models, errs)
|
||||||
|
|
||||||
|
# LUT (monoton, per Interpolation der Quantilpaare)
|
||||||
|
order = np.argsort(t)
|
||||||
|
tt, rr = t[order], r[order]
|
||||||
|
xs = np.arange(256) / 255.0
|
||||||
|
lut = np.interp(xs, tt, rr)
|
||||||
|
lut = np.maximum.accumulate(lut)
|
||||||
|
luts.append(np.clip(np.round(lut * 255.0), 0, 255).astype(np.uint8))
|
||||||
|
|
||||||
|
print("Fit pro Kanal (Fehler = RMS in 0..255, kleiner ist besser)")
|
||||||
|
for cname in CHANNELS:
|
||||||
|
models, errs = results[cname]
|
||||||
|
print(f" {cname}:")
|
||||||
|
print(f" gain : g={models['gain']['g']:.4f}"
|
||||||
|
f" RMS {errs['gain']:.2f}")
|
||||||
|
print(f" linear : g={models['linear']['g']:.4f} o={models['linear']['o']:+.4f}"
|
||||||
|
f" RMS {errs['linear']:.2f}")
|
||||||
|
print(f" gamma : g={models['gamma']['g']:.4f} gamma={models['gamma']['gamma']:.4f}"
|
||||||
|
f" RMS {errs['gamma']:.2f}")
|
||||||
|
|
||||||
|
# Bestes gemeinsames Modell nach Summe der Fehlerquadrate waehlen,
|
||||||
|
# aber das einfachste nehmen, wenn es fast genauso gut ist.
|
||||||
|
total = {n: np.sqrt(np.mean([results[c][1][n] ** 2 for c in CHANNELS]))
|
||||||
|
for n in ("gain", "linear", "gamma")}
|
||||||
|
best = min(total, key=total.get)
|
||||||
|
if total["gain"] <= total[best] + 1.0:
|
||||||
|
best = "gain"
|
||||||
|
elif total["linear"] <= total[best] + 1.0 and best == "gamma":
|
||||||
|
best = "linear"
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Gesamtfehler gain {total['gain']:.2f} linear {total['linear']:.2f}"
|
||||||
|
f" gamma {total['gamma']:.2f} -> empfohlen: {best}")
|
||||||
|
if total[best] > 4.0:
|
||||||
|
print("ACHTUNG: Fehler ist hoch - die Abweichung ist wohl nicht durch ein einfaches")
|
||||||
|
print("Modell erklaerbar (oder die Bildinhalte unterscheiden sich zu stark).")
|
||||||
|
print("Mit --palette die Flaechenfarben ansehen, bevor die Werte uebernommen werden.")
|
||||||
|
print()
|
||||||
|
print("GLSL-Zeile fuer VideoWidget.cpp (im Fragment-Shader auf 'color' anwenden):")
|
||||||
|
P = [results[c][0][best] for c in CHANNELS]
|
||||||
|
if best == "gain":
|
||||||
|
v = ", ".join(f"{p['g']:.4f}" for p in P)
|
||||||
|
print(f" color = clamp(color * vec3({v}), 0.0, 1.0);")
|
||||||
|
elif best == "linear":
|
||||||
|
g = ", ".join(f"{p['g']:.4f}" for p in P)
|
||||||
|
o = ", ".join(f"{p['o']:.4f}" for p in P)
|
||||||
|
print(f" color = clamp(color * vec3({g}) + vec3({o}), 0.0, 1.0);")
|
||||||
|
else:
|
||||||
|
g = ", ".join(f"{p['g']:.4f}" for p in P)
|
||||||
|
e = ", ".join(f"{p['gamma']:.4f}" for p in P)
|
||||||
|
print(f" color = clamp(vec3({g}) * pow(color, vec3({e})), 0.0, 1.0);")
|
||||||
|
|
||||||
|
if args.lut:
|
||||||
|
with open(args.lut, "w", encoding="utf-8") as f:
|
||||||
|
f.write("in,R,G,B\n")
|
||||||
|
for i in range(256):
|
||||||
|
f.write(f"{i},{luts[0][i]},{luts[1][i]},{luts[2][i]}\n")
|
||||||
|
print(f"\nLUT gespeichert: {args.lut}")
|
||||||
|
|
||||||
|
if args.save_corrected:
|
||||||
|
x = test.astype(np.float64) / 255.0
|
||||||
|
out = np.empty_like(x)
|
||||||
|
for ci, cname in enumerate(CHANNELS):
|
||||||
|
out[..., ci] = apply_model(best, results[cname][0][best], x[..., ci])
|
||||||
|
Image.fromarray(np.round(out * 255.0).astype(np.uint8)).save(args.save_corrected)
|
||||||
|
print(f"Korrigiertes Testbild gespeichert: {args.save_corrected}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Hinweis: Weichen die Mittelwerte stark ab, obwohl beide Bilder denselben")
|
||||||
|
print("Inhalt zeigen, ist die Korrektur sinnvoll. Sind die Bilder praktisch")
|
||||||
|
print("gleich (Verhaeltnis ~1.0, Gain ~1.0), liegt der Unterschied NICHT in")
|
||||||
|
print("den Bilddaten - dann haben beide Bilder denselben Weg genommen.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
uartscope_20261017_021812.png
Normal file
BIN
uartscope_20261017_021812.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
Reference in New Issue
Block a user