Docker Proxy

Mein Docker ist mittlerweile hinter einem Proxy. Damit ich Docker jedoch dazu bringe, diesen Proxy zu verwenden, ist ein bisschen Konfigurationsarbeit zu erledigen.

Auf meinem Ubuntu Server musste ich folgenden Pfad anlegen:

/etc/systemd/system/docker.service.d/

Unter diesem Ordner legt eine Datei an, die „http-proxy.conf“ heißt. In dieser Datei schreibt ihr die Details eures Proxies:

[Service]
Environment="HTTP_PROXY=http://proxy:3333"
Environment="HTTPS_PROXY=http://proxy:3333"
Environment="NO_PROXY=localhost,127.0.0.1"

Das Ganze erfordert nun einen Neustart von Docker:

sudo systemctl daemon-reload
 $ sudo systemctl restart docker

Nun sollten die Images hinter eurem Proxy gezogen werden können.

Folgende Meldung habe ich immer bekommen, ehe ich den Proxy eingerichtet hatte:

Pulling image (image/image:latest)...
ERROR: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

Kavita Update

In einen meiner letzten Beiträge habe ich niedergeschrieben, wie Kavita, ein E-Book Server, installiert werden kann. Natürlich muss dieser immer wieder Updates erhalten. Auch wenn Kavita selbst eine Update-Seite hat, so habe ich dies dennoch etwas anders gehandhabt. Vorteil meiner Kavita Update Variante ist, dass ich ein schnell verfügbares Backup habe, auf das ich direkt zurückgreifen kann, wenn das Update fehlschlägt.
Wer Kavita wie in meinem Beitrag installiert hat, kann hier einfach folgen. Andere müssten gegebenenfalls kleinere Anpassungen treffen.

Zuerst müssen wir den Dienst stoppen, was wir mit systemctl schnell erledigt haben und gehen in das Elternverzeichnis von Kavita.

systemctl stop kavita
cd /opt

in /opt sollte ein Verzeichnis namens Kavita existieren. In diesem Ordner sind alle benötigten Dateien für unseren E-Book Server. Diesen moven wir weg und holen uns die neuste Version von der Kavita Seite, zum Zeitpunkt dieses Beitrages ist es die Version 0.8.2.

mv Kavita Kavita-
wget https://github.com/Kareadita/Kavita/releases/download/v0.8.2/kavita-linux-x64.tar.gz

Sobald wir das gezippte Archiv heruntergeladen und entpackt haben, sollte ein neues Kavita Verzeichnis unter /opt existieren. In diesem müssen wir das config Verzeichnis löschen, damit wir unsere alten Einstellungen und die Datenbank übernehmen können. Ich nehme schonmal vorweg, das es nicht über ein Copy & Paste hinausläuft.

tar xfvz kavita-linux-x64.tar.gz
rm -rf Kavita/config/
cp -pr Kavita-/config/ Kavita

Das Update ist soweit schon durch, fehlen nur noch die E-Books, die wir vorher auf den Server hatten. Diese können ebenfalls mit einem copy in unsere neue Kavita Umgebung abgelegt werden. Bei mir es nur das „books“ Verzeichnis, es mag bei euch etwas anders aussehen.

cp -p Kavita-/books/ Kavita

Zum Schluss vergeben wir noch die korrekten Berechtigungen, damit unser kavita User den Server starten und auf die weiteren Dateien bzw. Ordner zugreifen kann, gefolgt vom Start des Servers.

chmod 754 kavita:kavita
systemctl start kavita
systemctl status kavita

externe USB-SSD in Proxmox einbinden

In diesem Beitrag zeige ich euch, wie ihr eine externe USB-SSD in Proxmox einbinden könnt. Nur ein einfaches „Plug and Play“ ist es in diesem Fall nicht. Die Disk muss vorweg korrekt formatiert und gemountet werden, damit Proxmox die neue Platte finden kann – Proxmox kann dies nicht out of the box.

Als erstes ermitteln wir die USB-SSD mit fdisk. Meist ist sie als eine /dev/sda, wie in meine Fall, oder ähnlich eingebunden.

fdisk -l

Disk /dev/sda: 1.82 TiB, 2000365371904 bytes, 3906963617 sectors
Disk model: Extreme 55AE
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 1048576 bytes
Disklabel type: gpt
Disk identifier: 5623AD37-4AA9-4134-BC1A-565998A00786

Device     Start        End    Sectors  Size Type
/dev/sda1   2048 3906961568 3906959521  1.8T Linux filesystem

/dev/sda1 ist die Partition und diese formatieren wir in eine ext4 Dateisystem.

mkfs.ext4 /dev/sda1

mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done
Creating filesystem with 488369940 4k blocks and 122093568 inodes
Filesystem UUID: c1408055-3603-45c5-8e7a-ca793faa6011
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
        4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
        102400000, 214990848

Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

Verläuft die Formatierung erfolgreich, mounten wir die Platte in ein angelegtes Verzeichnis hinein. Den Mount-Point könnt ihr euch dabei selber aussuchen.

mkdir /mnt/ext_ssd
mount /dev/sda1 /mnt/ext_ssd/

Nachdem die Platte gemountet ist, ist es nun über Proxmox möglich, diese einzubinden. Jedoch würde der Mount nach einem Neustart wieder verschwinden. Abhilfe schafft ein Eintrag in die /etc/fstab. Dazu ermitteln wir die UUID unserer neu erzeugten Partition

root@pve:/mnt/ext_ssd# blkid | grep sda1
/dev/sda1: UUID="c1408055-3603-45c5-8e7a-ca793faa6011" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="Extreme SSD" PARTUUID="367c946e-fc02-4fd0-be7d-085323bbc320"

root@pve:/mnt/ext_ssd# systemctl daemon-reload

Haben wir die UUID ausfindig gemacht, setzen wir einen neuen Eintrag in /etc/fstab. Ihr könnt dabei die Zeile von unten kopieren, müsst allerdings die UUID und den Mount-Point, hier /mnt/ext_sdd, an eure Gegebenheiten anpassen. Falls ihr ein anderes Dateisystem als ext4 genommen habt, müsst ihr auch diese anpassen.

root@pve:/mnt/ext_ssd# vi /etc/fstab
UUID=c1408055-3603-45c5-8e7a-ca793faa6011 /mnt/ext_ssd ext4 auto,nofail,sync,users,rw   0   0

Nach dem die Zeile gesetzt ist, überprüft, ob die Partition gemountet werden kann

root@pve:/mnt/ext_ssd# mount -a

Sollte kein Fehler kommen und im besten Falle nur die nachfolgende Meldung, könnt ihr den daemon neu laden und die Partition sollte bei jedem Neustart mit gemountet werden.

mount: (hint) your fstab has been modified, but systemd still uses<br>the old version; use 'systemctl daemon-reload' to reload.

Nach der relativ einfachen Initialisierung der USB-SSD auf Betriebssystemebene, machen wir die Platte jetzt noch Proxmox bekannt, damit wir dort Images, ISOs, etc. ablegen können. Geht dazu auf oberste Ebene „Rechenzentrum“ und fügt einen neuen Storage vom Typ „Verzeichnis“ hinzu. Setzt eine ID und in das Verzeichnis-Feld den Mount-Point, sowie die Labels.

Auf euren Knoten sollte der neu eingefügte Storage sichtbar sein. Dort könnt ihr sehen, wieviel Speicher noch frei ist und andere wichtige Informationen

Damit könnt ihr eure externe SSD als Datenspeicher in Proxmox benutzen.

Weblog Auswertung

Es gibt genügend Weblog Dateien, die einem ungefilterte Informationen zu bestimmten Dingen geben. Eine davon ist die von meiner emslandmap auf unser-schoenes-emsland. Auch wenn ich die Logs in diesem Fall selber schreiben lasse, so ist die Masse nicht immer zielführend für Auswertungen und ein simples durchforsten ist meist aufwendig. Zum Glück gibt es Möglichkeiten die Logs mit einer Shell zielgenauer zu filtern. Dies hab ich mit dem Skript aus diesem Beitrag gemacht.

Am Anfang stehen ein paar Variablen, die vorher gesetzt werden. Das Skript sucht nach Zeilen mit dem Schlagwort „REMOTE IP“. Wie solch eine Zeile aussieht seht ihr hier:

2024-01-18T19:51:55+01:00 [INFO] : [get.php][0c0a67801091f35ceb028810110daf3a44ddaebc]REMOTE IP: 8.8.8.8

Neben der IP lasse ich noch ebenfalls das Datum + Uhrzeit rausziehen. Das passiert mit cut -s -d: -f1-3,6. Der Delimiter wurde auf „:“ geändert. Ausgegeben werden die ersten 3 Felder für das Datum + Uhrzeit, sowie die 6 für die IP. Mittels uniq -s 34 werden gleiche IP Adressen zusammengezählt. Diese Info geht in die .CSV Datei.

Als nächstes ermittle ich die Anzahl der eindeutigen IP Adressen und schreibe es in die Variabel uniqip. Wir nehmen Teile des ersten Befehls mit dem Unterschied, dass die IPs sortiert und mit wc -l gezählt sind. Auch diese Info kommt in die .CSV Datei.

Die jetzt vollständige Datei geht mit einer Mail an die angegebene Adresse. Zum Schluss noch ein paar Aufräumarbeiten, in der wir die .CSV Datei löschen und die Weblog umbenennen.

#!/bin/bash

logfile=/pfad/name.log
archivfile=/pfad/name_$(date +"%d_%m_%Y").log
textfile=emslandmap_$(date +"%d_%m_%Y").csv
receiver=receiver@tld.de
sender=sender@tld.de

grep "REMOTE IP" ${logfile} | cut -s -d: -f1-3,6 | uniq -s 34 >> ${textfile}
uniqip=$( grep "REMOTE IP" ${logfile} | cut -s -d: -f6| sort -n | uniq | wc -l )
echo "Einzigartige IP-Adressen: ${uniqip}" >> ${textfile}

echo "Auswertung " | mail -r ${sender} -s "Betreff Webaufrufe" -a ${textfile} ${receiver}

rm -f ${textfile}
mv ${logfile} ${archivfile}

exit 0

Das Skript speichern wir ab und legen es in einem Verzeichnis, um es per Cronjob automatisch ausführen zu lassen.

Virtualbox – Internes Netzwerk

Virtualbox stellt einige Netzwerkmodis bereit, unteranderem das „Internes Netzwerk“. Dieses Netzwerk bietet den Vorteil, dass die virtuellen Maschinen, die dem selben Netzwerk zugeordnet sind, nur untereinander kommunizieren können. Keine Verbindung nach draußen oder dem Host, ein komplett für sich gekapseltes Netz. Für mein Pentest Labor wollte ich genau solch ein Netzwerk, sodass die Maschinen von vulnhub, etc. nicht auf andere Maschinen zugreifen können. Das neu aufgebaute Netz sollte später wie im Schaubild agieren. Über eine virtuelle Switch sind die 2 VMs verbunden. Der Host befindet sich in einem anderen Netzbereich und hat somit keinen Zugriff.

Aufbau des internen Netzes von Virtualbox

Die untere Tabelle zeigt euch, was jeweils mit dem gewählten Netzwerkmodus möglich ist. Das „Internes Netzwerk“ zeigt hier ganz klar, dass nur mit anderen VMs kommuniziert werden kann.

ModeVM -> HOSTVM <- HOSTVM1 < – > VM2VM -> Net/LANVM <- Net/LAN
Host-only+++
Internes Netzwerk+
Netzwerkbrücke+++++
NAT+Port forward+Port forward
NATservice+Port forward++Port forward

Um eure VM dafür vorzubereiten, muss der Netzwerkadapter abgeändert werden. Wählt dazu den Typ „Internes Netzwerk“. Den Namen könnt ihr frei wählen oder nehmt den Default „intnet“. Ich habe meines „hacking“ genannt.

Das wars eigentlich schon für die VM. Wer mir bis hierhin gefolgt ist und die Maschinen schon gestartet hat, wird aber schnell feststellen, dass das neue Interface keine IP Adresse hat und somit die VMs eben nicht miteinander kommunizieren können. Es gibt nun zwei Möglichkeiten, statische IP Adressen vergeben oder einen DHCP Server mittels Virtualbox aufsetzen.

Da der DHCP Server die komfortabelste Lösung ist, habe ich einen neuen erstellt, der für das interne Netwerk „hacking“ arbeitet.

c:\Program Files\Oracle\VirtualBox>VBoxManage.exe dhcpserver add --network=hacking --server-ip=192.168.180.2 --netmask=255.255.255.0 --lower-ip=192.168.180.50 --upper-ip=192.168.180.100 --enable

Startet jetzt die VMs. Sie sollten nun eine Adresse im definierten Bereich erhalten haben. Damit hättet ihr in VirtualBox ein Netzwerk von Typ „Internes Netzwerk“ angelegt.

Weitere optionale VirtualBox Befehle

Mit diesem Befehl zeigt ihr alle verfügbaren DHCP Server an.

VBoxManage.exe list dhcpservers

Mit intnets werden alle Netze angezeigt, die vom Typ „internes Netzwerk“ sind.

VBoxManage.exe list intnets

Kavita auf dem Raspberry Pi installieren

Wer viele eBooks besitzt und diese gerne zentralisiert speichern möchte, hat vielleicht schon etwas von Kavita gehört. Es ist ein open-source Buchserver der viele eBook-Formate lesen kann. Dabei werden unterschiedliche Plattformen wie Linux, Windows und Docker unterstützt. Auch für den Raspberry Pi gibt es eine ARM Version, die ich für meinen Raspberry PI 4 benötige.
Die Installation ist relativ einfach. Ladet mit wget die richtige Version für euch herunter. Nicht zwingend erforderlich aber doch von Vorteil ist die Anlage eines neuen Benutzers, der die erforderlichen Rechte auf dem Verzeichnis hat.

cd /opt/
sudo wget https://github.com/Kareadita/Kavita/releases/download/v0.7.8/kavita-linux-arm.tar.gz
sudo tar xfvz kavita-linux-arm.tar.gz
sudo useradd kavita
sudo passwd kavita
sudo chown -R kavita:kavita Kavita/

Wer Kavita nicht immer manuell starten möchte, kann auch einen eigenen systemd Service generieren. Legt dafür einfach eine neue kavita.service Datei unter /etc/systemd/system an und kopiert den nachfolgenden Block hinein.

[Unit]
Description=Kavita Server
After=network.target

[Service]
User=kavita
Group=kavita
Type=simple
WorkingDirectory=/opt/Kavita
ExecStart=/opt/Kavita/Kavita
TimeoutStopSec=20
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target

Danach könnt ihr die Systemd Konfigurationsdateien und Units neu laden. Die zweite Codezeile startet Kavita und die dritte fügt es zum Startup hinzu.

sudo systemctl daemon-reload
sudo systemctl start kavita.service
sudo systemctl enable kavita.service

Falls ihr keinen neuen Service definieren wollt, könnt ihr Kavita wie folgt, über den vorher neu erstellten User, starten.

/opt/Kavita# sudo su kavita
kavita@raspberrypi4:/opt/Kavita$ ./Kavita
[Kavita] [2023-10-12 20:34:23.500 +02:00  1] [Information] API.Services.TaskScheduler Scheduling reoccurring tasks
[Kavita] [2023-10-12 20:34:23.686 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling Scan Library Task for daily
[Kavita] [2023-10-12 20:34:24.433 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling Backup Task for daily
[Kavita] [2023-10-12 20:34:24.607 +02:00  1] [Information] API.Services.TaskScheduler Scheduling Auto-Update tasks
[Kavita] [2023-10-12 20:34:24.615 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling stat collection daily
[Kavita] [2023-10-12 20:34:24.880 +02:00  4] [Information] API.Program Running Migrations
[...]

Öffnet nun die Seite mit http://raspberrypi4:5000 in eurem Browser. Kavita lauscht standardmäßig auf den Port 5000. Sollte dies nicht klappen, überprüft, ob der Prozess überhaupt gestartet ist. Im nächsten Codeblock zeige ich einmal den Weg mittels systemctl und den ohne. Mit Systemctl sollte ein Active (running) vorhanden sein, sowie eine PID. Unabhängig, ob ihr das mit systemctl macht, könnt ihr auch mit „ps“ überprüfen, ob ein Prozess läuft. Mittels „netstat“ überprüft ihr letztendlich, ob ein Service auf den Port 5000 lauscht.

]pi@raspberrypi4:/opt/Kavita# sudo systemctl status kavita.service
● kavita.service - Kavita Server
   Loaded: loaded (/etc/systemd/system/kavita.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2023-10-12 20:37:07 CEST; 2s ago
 Main PID: 27434 (Kavita)
    Tasks: 16 (limit: 4915)
   CGroup: /system.slice/kavita.service
           └─27434 /opt/Kavita/Kavita

Okt 12 20:37:07 raspberrypi4 systemd[1]: Started Kavita Server.


[Do Okt 12]pi@raspberrypi4:/opt/Kavita# ps aux | grep Kavita
kavita   27434  103  3.4 900748 134900 ?       Rsl  20:37   0:13 /opt/Kavita/Kavita
pi       27585  0.0  0.0   7356   552 pts/0    S+   20:37   0:00 grep --color=auto 27434


[Do Okt 12]pi@raspberrypi4:/opt/Kavita# sudo netstat -tapen | grep 27434
tcp6       0      0 :::5000                 :::*                    LISTEN      1001       75764786   27434/Kavita
tcp6       0      0 192.168.170.33:35342    140.82.121.5:443        VERBUNDEN   1001       75766833   27434/Kavita
[Do Okt 12]pi@raspberrypi4:/opt/Kavita#

Sollte ein Prozess existieren und dieser auf Port 5000 lauschen, ist die Installation abgeschlossen und ihr solltet auf die Startseite kommen. Legt zuerst einen neuen User an und definiert danach eine neue Library, welches auf euer Verzeichnis mit den eBooks zeigt. Das Einlesen könnte, je nach Anzahl eurer eBooks, etwas dauern. Ihr habt die Wahl eure eBooks direkt im Browser zu lesen oder diese lokal auf eure Geräte herunterzuladen.

Hier findet ihr noch die Github Seite des Projekts: https://github.com/Kareadita/Kavita

Wer ein Update machen möchte findet hier den richtigen Beitrag: Kavita Update

Zahlen erkennen mit Python

In diesem Artikel möchte ich zeigen, wie man Zahlen mit Python erkennen kann. Das Touchpad vom Laptop benutze ich, um Zahlen zu zeichnen. Dafür verwende ich keras und Tensorflow und einen eigenen Datensatz zum Trainieren und Validieren.

Datensatz erstellen

Für den Datensatz verwende ich folgenden Python Code. Die Ordnerstruktur muss vorher angelegt sein:

import cv2 as cv
import pyautogui
import numpy as np
import keyboard
import string
import random
from time import sleep


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x

def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    dim=(28,28)

    for n in range(0,10,1):
        for d in range(0,1,1):
            print(f'Zahl: {n} \nAnzahl: {d}')
            # create np array with a size of max windows resolution
            y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
            # make array image black
            pic = np.full_like(y,[0,0,0])
            while not keyboard.is_pressed('esc'):
                # get mouse position
                mouseX, mouseY = pyautogui.position()
                #print(mouseX,mouseY)
                # make the drawing lines bigger
                for i in range(-5,5):
                    pic[mouseY ,mouseX + i] = [255,255,255]
                    pic[mouseY + i, mouseX] = [255,255,255]
                      
            sleep(0.1)
            # find min and max of each axis(x,y)
            min_y,max_y,min_x,max_x = find_minmax(pic)
            picresized = pic[min_y:max_y,min_x:max_x]
            picresized = cv.resize(picresized,dim)
            randomstring = ''.join(random.choices(string.ascii_lowercase,k=8))
            #print(f"create file {n}/{d}{randomstring}.png")
            #cv.imwrite(f'./{n}/{d}{randomstring}.png',picresized)
            print(f"create file Test/{d}{randomstring}.png")
            cv.imwrite(f'./Test/{d}{randomstring}.png',picresized)
        


if __name__ == '__main__':
    main()

Für jede einzelne Ziffer gibt es mindestens 50 Datensätze, insgesamt habe ich 554 Bilder erstellt. Dabei könnte man die Prozedur in 3 Teile gliedern. Die erste Prozedur besteht darin die Zahl zu zeichnen. Dabei entsteht ein Bild von der Größe des Displays, bei mir 1920×1080. Dieses Bild wird dann reduziert auf den Zahlenbereich, bevor es dann auf eine Größe von 28×28 reduziert wird. Damit hätten wir einen vollständigen Datensatz, mit dem wir unser Model trainieren können

Das Model erstellen und trainieren

Für die Erstellung des Models verwende ich Tensorflow und Keras.Sequential. Dabei bin ich nach dem Tutorial von tensorflow.org vorgegangen.

import numpy as np
import matplotlib.pyplot as plt
import PIL
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

import pathlib

def visualize_training_results(history,epochs):
    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']

    loss = history.history['loss']
    val_loss = history.history['val_loss']

    epochs_range = range(epochs)

    plt.figure(figsize=(8, 8))
    plt.subplot(1, 2, 1)
    plt.plot(epochs_range, acc, label='Training Accuracy')
    plt.plot(epochs_range, val_acc, label='Validation Accuracy')
    plt.legend(loc='lower right')
    plt.title('Training and Validation Accuracy')

    plt.subplot(1, 2, 2)
    plt.plot(epochs_range, loss, label='Training Loss')
    plt.plot(epochs_range, val_loss, label='Validation Loss')
    plt.legend(loc='upper right')
    plt.title('Training and Validation Loss')
    plt.show()

data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Modelcreation')

image_count = len(list(data_dir.glob('*/*.png')))
print(image_count)

batch_size = 32
img_height = 28
img_width = 28

# create dataset
train_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='training',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

val_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='validation',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

class_names = train_ds.class_names
print(class_names)

# Configure dataset for performance
AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(20).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

# Standardize the data
normalization_layer = layers.Rescaling(1./255)

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
image_batch, labels_batch = next(iter(normalized_ds))
first_image =  image_batch[0]
print(np.min(first_image), np.max(first_image))

num_classes = len(class_names)

# Create the model
model = Sequential([
    layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
    layers.Conv2D(16,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(32,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(64,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dense(num_classes)
])
# compile model
model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

model.summary()

## Train the model

epochs = 10
history = model.fit(
    train_ds,
    validation_data = val_ds,
    epochs=epochs
)

## Visualize Training results
visualize_training_results(history, epochs)

#Predict on new data
data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Test/0nzzlfidh.png')
img = tf.keras.utils.load_img(
    data_dir, target_size=(img_height, img_width)
)

img_array = tf.keras.utils.img_to_array(img),
img_array = tf.expand_dims(img_array[0], 0) # Create a batch
predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This number most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)

# save model
model.save('C:/Users/Willkommen/Desktop/handdigits/ownmodel.h5')

Wir laden die Daten und splitten die Datensätze im Verhältnis 80% zu 20% in Trainings- und Validierungsdaten. Der Klassenname bezieht sich auf den Ordnername, also die Zahlen von 0 – 9. Die Datensätze ziehen wir uns von der Festplatte in den Memory Cache. Dies hat den Vorteil das etwaige I/O auf der Platte nicht zu einem Flaschenhals führen, wenn wir das Model trainieren.
Da der RGB Channel Werte zwischen 0 – 255 hat, normalisieren wir diese noch, sodass ein Wert zwischen 0 und 1 besteht.
Nach der Normalisierung erstellen wir unser Keras Sequential Model mit drei convolution Blöcken. Jeder hat zusätzlich ein max pooling layer.
Zum Kompilieren benutzen wir den Adamoptimizer und die SparseCategoricalCrossentropy loss Funktion.
In insgesamt 10 Epochen trainieren wir unser Model. Zur Visualisierung des Trainingsergebnisses mittels matplotlib gibt es die Methode visualize_training_results.
Um unser gerade neu erstelltes Model zu testen, benutzen wir das Model auf neu erstellte Testdaten.

Nachfolgend könnt ihr die Validierung vom Training sehen. Das Model scheint dabei gut zu performen.

Modelvalidierung

Mit Touchpad zeichnen

Das Model für die Zahlenidentifizierung haben wir nun und wir könnten die Zahlen mit Python erkennen. Fehlt lediglich das Zeichnen mit dem Touchpad. Dafür verwende ich die Bibliothek „keyboard“. Diese ermittelt die aktuelle Mausposition und liefert X,Y Koordinaten. Jene X,Y Koordinaten ändern die Werte im Numpy Array und haben statt dem Wert [0,0,0] den Wert [255,255,255]. Wie auch beim Trainieren des Models, entspricht das erste Bild der Gesamtgröße des Bildschirms. Das zweite ist nur der Zahlenbereich und das dritte und finale Bild, der Zahlenbereich auf die Größe 28,28 reduziert.
Nachdem wir nun das 28×28 Bild-Array haben, rufen wir die Methode predict_num(img) auf. In der Methode wird das Bild noch einmal auf die korrekte Größe gebracht, ehe es dann durch das Model bewertet wird.

import cv2 as cv
import keyboard
import pyautogui
import numpy as np
import tensorflow as tf
from keras.models import load_model

model = load_model('ownmodel.h5')

def predict_num(img):
    # reshape for model compatibility
    img = img.reshape(1,28,28,3)
    #predicting the class
    img = tf.expand_dims(img[0], 0) # Create a batch
    predictions = model.predict(img)
    score = tf.nn.softmax(predictions[0])

    print(
    "You draw number {} with a {:.2f} percent confidence."
    .format(np.argmax(score), 100 * np.max(score))
    )


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x
 
def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    firstrun = True
    dim=(28,28)

    # create np array with a size of max windows resolution
    y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
    # make array image black
    pic = np.full_like(y,[0,0,0])
    
    while not keyboard.is_pressed('esc'):
        if firstrun:
            print("Draw your number now and confirm with ESC")
            firstrun=False

        # get mouse position
        mouseX, mouseY = pyautogui.position()
        # make the drawing lines bigger
        for i in range(-5,5):
            pic[mouseY ,mouseX + i] = [255,255,255]
            pic[mouseY + i, mouseX] = [255,255,255]

    cv.imwrite('./original.png',pic)

    # find number and reduce picturesize to a minimum
    min_y,max_y,min_x,max_x = find_minmax(pic)
    picresized = pic[min_y:max_y,min_x:max_x]
    cv.imwrite('./originalcut.png',picresized)

    # resize image to (28,28)
    picresized = cv.resize(picresized,dim)
    cv.imwrite('./resized.png',picresized)

    #predict
    predict_num(picresized)

if __name__ == '__main__':
    main()

Testbeispiel

Hier findet ihr ein Beispiel, bei dem ich die 3 über das Touchpad gezeichnet habe. Folgend findet ihr das original Bild, nur den Zahlenbereich und den Zahlenbereich auf 28×28 herunterskaliert. Das Model hat dabei folgendes ausgegeben:

Draw your number now and confirm with ESC
1/1 [==============================] - 0s 417ms/step
You draw number 3 with a 99.95 percent confidence.

Und hier jetzt einmal die Bilder

Den ganzen vollständigen Code findet ihr bei mir auf github: https://github.com/stevieWoW/neuronalnetworkmousepadnumber

opencv template matching

Viele von euch kennen wahrscheinlich die „Where’s Waldo“, bzw. auf deutsch, „Wo ist Walter“ Bilder. Bei den Bildern geht es einzig darum Walter unter den Unmengen anderer Personen zu finden. Teils gar nicht so einfach. Ich habe hier ein Beispiel, womit wir Walter anhand opencv template matching finden und ihn kenntlich machen. Dazu habe ich 2 Bilder vorbereitet. Das erste ist das Gesicht vom Walter.

Wo ist Walter Gesicht

Nach diesem kleinen Schnipsel wird opencv nachher das komplette Bild absuchen.

Wo ist Walter

Wer natürlich Lust hat, kann Walter vorher suchen. Diese Arbeit musste ich vorab schon machen, um das Gesicht als Bild zu haben, die Arbeit habt ihr euch also schon gespart :).

Voraussetzungen

An sich gibt es nicht viele Voraussetzungen. Lediglich opencv, matplotlib und numpy müssen installiert sein.

pip install opencv-python
pip3 install matplotlib
pip3 install numpy

template matching

Für das template matching gibt es in opencv die Funktion cv.matchTemplate(). Diese erwartet das komplette Bild, den Schnipsel und eine Vergleichsmethode als Parameter übergeben. Vergleichsmethoden können TM_SQDIFF, TM_SQDIFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_CCOEFF und TM_CCOEFF_NORMED sein. Jede Methode verwendet einen unterschiedlichen Berechnungsweg, um an das Ziel zu gelangen. Das Ergebnis ist ein weiteres numpy.ndarray der Größe (W-w +1) x (H -h +1). W/H ist die Größe des vollständigen Bilds und w/h die des Schnipsels. Hier einmal als Vergleich.

vollständige Bild(H x W): 
print(img_rgb.shape)
-> (1760, 2800, 3)

Schnipsel(h x w): 
print(face.shape)
-> (22, 22)

Ergebnis (H - h + 1) x (W - w + 1): 
print(res.shape)
-> (1739, 2779)

Die Vergleichsmethode schiebt das Schnipsel über das große Bild und vergleicht jeweils die gerade übereinanderliegenden Stellen von der Größe w x h und schreibt den Wert in das res numpy.ndarray. Bei allen Vergleichsmethoden außer TM_SQDIFF oder TM_SQDIFF_NORMED gilt, je höher der Wert desto mehr sieht sich die gerade überlappende Stelle ähnlich. Für diesen Beitrag habe ich TM_CCOEFF_NORMED verwendet, spielt letzten Endes aber keine große Rolle.

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

face = cv.imread('waldoface.jpg',0)
img_rgb = cv.imread('whereiswaldo.jpg')
img_gray = cv.imread('whereiswaldo.jpg',0)
w,h = face.shape[::-1]

res = cv.matchTemplate(face,img_gray,cv.TM_CCOEFF_NORMED)

Nachdem wir nun das Ergebnis der Vergleichsmethode haben, können wir uns den besten Treffer mit der Funktion cv2.minMaxLoc() aus dem ndarray extrahieren.

min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
print(min_val, max_val, min_loc, max_loc)
-> -0.5772402882575989 0.9950668215751648 (912, 212) (1169, 298)

cv2.minMaxLoc() sucht sich den kleinsten und größten Wert mit jeweils dem Index raus. Da unser ndarray aus mehr als 1000 Zeilen und Spalten besteht, verdeutliche ich die funktionsweise von cv2.minMaxLoc() kurz anhand eines kleineren Arrays.

array = [
    [1,4,6,8,9], #0 Dimension
    [2,5,4,6,5], #1
    [6,4,3,6,5]  #2
    #0,1,2,3,4
]

min_val,max_val,min_indx,max_indx=cv2.minMaxLoc(array)
print(min_val,max_val,min_indx,max_indx)
-> 1.0, 9.0, (0, 0), (4, 0)

Anhand dieses Beispiels können wir sehen, dass der kleinste Wert (=1.0) das 0 Element im der 0 Dimension ist. Ein Array startet üblicherweise immer mit 0. Der größte Wert(=9.0) ist das 4 Element in der 0 Dimension.
Genau das gleiche machen wir mit unserem res ndarray. Unser Waldo müsste dementsprechend beim Index (1169, 298) liegen. Wir gehen also zu diesem Punkt im Bild und zeichnen von dort an ein Rechteck der Größe 22 x 22 (w x h).

top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
print(top_left, bottom_right)
-> (1169, 298) (1191, 320)

cv.rectangle(img_rgb,top_left,bottom_right, (0, 255, 0), 2)
cv.putText(img_rgb,'Waldo', bottom_right, cv.FONT_HERSHEY_TRIPLEX,3, (0,255,0),3)
plt.imshow(img_rgb)
plt.suptitle('Ergebnis')
plt.show()

Das Ergebnis zeigen wir uns dann mittels matplotlib an. Wer das Bild noch abspeichern möchte kann dies mittels opencv machen.

cv.imwrite('Waldogefunden.jpg', img_rgb)

Damit haben wir Waldo mittels template matching im Bild gefunden und kenntlich gemacht.

template Matching Waldo gefunden

Den vollständigen Code könnt ihr auf meinem Github finden.

Nabu Vogelliste als Pandas Dataframe

Die Nabu hält alljährlich eine Gartenvögelzählung ab, bei denen unter anderem private Personen gesichtete Vögel an einem Tag zählen und übermitteln können. Diese Liste stellt die Nabu dann auf deren Internetpräsenz zur Verfügung. Angezeigt wird die Vogelart, die Anzahl, die Vögel pro Garten und ein Vergleich zum Vorjahr. Filtern könnt ihr nach Jahr, Bundesland, Vogelart und Landkreis/Stadt.
Dies vorab als kleine Beschreibung, um was für eine Liste es sich handelt, die wir uns zu eigen machen. Am einfachsten lässt sich die Nabu Vogelliste als Pandas Dataframe darstellen, wenn wir BeautifulSoup benutzen. Mit Hilfe dieser Bibliothek lassen sich XML und HTML Dokumente parsen. Für die Abarbeitung benötigen wir also eine URL, die wir für unseren Get-Request benutzen können. Mitinbegriffen sind in der unten angegebenen URL schon die Filtereinstellungen. So sind nur die Ergebnis für das Jahr 2021 enthalten, die im Bundesland Niedersachsen und dem Ort Emsland(034540000000) vorkommen. Wer die komplette Liste haben möchte oder sich seine eigene URL generieren möchte, kann dies mit diesem Link machen.

https://www.nabu.de/tiere-und-pflanzen/aktionen-und-projekte/stunde-der-gartenvoegel/ergebnisse/15767.html?jahr=2021&bundesland=Niedersachsen&vogelart=&ort=034540000000

In unseren Python Code importieren wir die benötigten Bibliotheken. Eventuell müsst ihr BeautifulSoup noch nachträglich installieren, darauf gehe ich hier nicht weiter drauf ein.

from bs4 import BeautifulSoup
import requests
import pandas as pd

Zuerst holen wir uns das HTML Dokument und initialisieren BeatifulSoup. Danach suchen wir nach einem <tbody> im HTML DOM und gehen die einzelnen Tabellenfelder durch. Das Ergebnis schreiben wir vorerst in eine Liste. Diese brauchen wir später für unseren Dataframe, da wir aus der Liste das Dataframe erzeugen.

tbody = soup.find('tbody')
vogel_df = pd.DataFrame()
vogelliste = list()

for i in tbody:
   
    try:
        vogelliste.append([
            i.find_next('tr').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText()
        ])

    except:
        pass

vogel_df = pd.DataFrame(vogelliste, columns=['Vogelart','Anzahl','Prozent_der_Gärten','Vögel_pro_garten','Vergleich_zum_Vorjahr','Vergleich_zum_Vorjahr(Prozent)'])

Eine Ausgabe des Dataframe würde folgendes ausgeben:

          Vogelart Anzahl Prozent_der_Gärten Vögel_pro_garten Vergleich_zum_Vorjahr Vergleich_zum_Vorjahr(Prozent) 
0     Haussperling   1500             78,79%             5,68                + 0,19                           + 4%  
1            Amsel    885             95,45%             3,35                + 0,21                           + 7%  
2        Kohlmeise    815             85,98%             3,09                + 0,68                          + 28%   
3        Blaumeise    700             81,82%             2,65                + 0,59                          + 28%     
4      Ringeltaube    599             61,74%             2,27                + 0,22                          + 11%  
..             ...    ...                ...              ...                   ...                            ...
79       Sturmmöwe      1              0,38%             0,00                     -                              0
80    Birkenzeisig      1              0,38%             0,00                  0,00                          + 27%   
81      Feldlerche      1              0,38%             0,00                  0,00                          + 27%   
82   Braunkehlchen      1              0,38%             0,00                     -                              0   
83  Waldbaumläufer      1              0,38%             0,00                     -                              0 

Das war es schon, um die Nabu Vogelliste als Pandas Dataframe zu erhalten. Relativ simpel und doch funktionstüchtig.

LED-Streifen mit offline Sprachassistenten steuern

Da ich mittlerweile eine eigene kleine Sprachbox in meinem Beitrag Offline Sprachassistent mit Python erstellt habe, sollte diese natürlich auch Aktionen ausführen. Als erste Aktion wollte ich meine LED-Streifen mit dem offline Sprachassistenten steuern. Dafür habe ich in dem oben genannten Beitrag schon viel Arbeit davon erklärt. Anstatt des Klemmbretts habe ich jetzt eine kleine Platine benutzt und die benötigten Komponenten verlötet. Die Platine habe ich dann in einem alten Raspberry PI Gehäuse verbaut. Bei den LED-Streifen habe ich darauf geachtet, das diese über USB und dementsprechend 5V laufen. Zum Einsatz kommen die Govee LED Streifen mit 3m, die ich bei dem Amazon Prime Days günstig geschossen hatte. Mit meiner Steuerung kann ich den Steifen an und aus schalten. Die Farbe kann ich entweder mit der App oder der Fernbedienung ändern.

Automatischer Start beim Boot

Damit ich die Sprachsteuerung nicht dauerhaft eigenhändig starten muss, habe ich mich dazu entschlossen, dass überprüft wird, ob das Startup Skript läuft und falls nicht, dass es automatisch gestartet wird. Dafür existieren die beiden Skripte run_speech.sh und speech.sh. Beide liegen im /etc/ Verzeichnis und sind ausführbar.

run_speech.sh ist als Cronjob angelegt, der jede 5 Minuten überprüft, ob speech.sh läuft. Sollte dieses aus irgendeinen Grund nicht laufen, startet run_speech.sh speech.sh und schiebt es in Hintergrund.

run_speech.sh
#!/bin/bash

proc='speech.sh'

pid=`ps aux | grep -w ${proc} | grep -v grep`
rc=$?

if [ ! $rc -eq 0 ]
then
        echo "starte speech"
        /etc/speech.sh &>> /var/log/speech/speech`date +%V`.log &
fi

exit

In speech.sh läuft eine Dauerschleife, die dauerhaft überprüft, ob das Python Skript für den Sprachassistenten läuft. Auch hier ist es der Fall, dass speech.py gestartet wird, falls es nicht läuft.

speech.sh
#!/bin/bash

name="speech.py"
waittime=5

if [ ! -d "/var/log/speech" ]
then
                mkdir "/var/log/speech"
fi

while true
do
        process=`ps aux | grep "${name}" | grep -v grep`
        rc=$?

        if [ ! $rc -eq 0 ]
        then
                echo "`date +%Y-%m-%d\ %T` INFO `basename $0` :: starting speech.py..." >> /var/log/speech/speech`date +%V`.log
                cd /home/pi/vosk-api/python/example
                /usr/bin/python3 /home/pi/vosk-api/python/example/speech.py &>> /var/log/speech/speech-detail`date +%V`.log &
                echo "`date +%Y-%m-%d\ %T` INFO `basename $0` :: speech.py was started" >> /var/log/speech/speech`date +%V`.log
        fi
        sleep ${waittime}
done

Den Quellcode, für den Sprachassistenten findet ihr im oben genannten Beitrag.

Video

Zum Schluss noch ein Video, das genau zeigt, wie ich die LED-Streifen mit dem offline Sprachassistenten steuern kann. Klar zu sehen ist, dass der Assistent nur dann reagiert, wenn er das Codewort herausgehört hat. In der speech-detail.log schreibt der Prozess die erkannten Wörter/Sätze nieder. Der Sprachassistenten ist soweit aufgebaut, dass dieser das Codewort nicht aus einem Satz herausfiltert. Dies macht ihn ein wenig anfällig auf im Hintergrund getätigte Gespräche, die das Mikrofone eventuell mit aufzeichnet.