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

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.

Kali Linux 2021.4 wurde veröffentlich

Mit dem beinahen Ende von 2021 wurde Kali Linux 2021.4 als letztes Release für dieses Jahr veröffentlicht. Seit dem 09.12 ist die Version nun verfügbar und beinhaltet folgende Änderungen:

  • Verbesserte Apple M1 Unterstützung
  • Größere Samba Kompatibilität
  • Package Manager Mirror konfigurieren
  • Kaboxer Themen Unterstützung
  • Neue Tools
  • Desktop & Themen Erweiterungen
  • Kali NetHunter Neuerungen
  • Kali ARM Neuerungen

Verbesserte Apple M1 Unterstützung

Seit Kali 2021.1ist Kali auf dem Apple Silicon Macs lauffähig. Mit 2021.4 unterstützt es nun auch die VMware Fusion Public Tech Preview, dank des 5.14 Kernels, der die Module für die virtuelle GPU bereitstellt. Ebenfalls sind die open-vm-tools auf dem neusten Stand. Der Kali Installer erkennt automatisch die Installation unter VMware und installiert direkt das open-vm-tools-desktop Package. Zurzeit ist dies unter Beobachtung von VMware, es könnten also weiterhin Fehler auftreten.
Virtuelle Maschinen auf dem Apple Silicon sind, wie zuvor, auf ARM64 Installationen limitiert.

Größere Samba Kompatibilität

Mit Kali 2021.4 ist der Samba Client nun zu so gut wie mit allen Samba Servern kompatibel, egal welche Version vom Protokoll benutzt wird. Diese Änderung sollte es vereinfachen, Schwachstellen in Samba Servern zu finden, ohne dafür Kali zu konfigurieren. Mittels den kali-tweaks unter der Rubrik Hardening könnt ihr die Änderungen wieder zurück auf die Default Einstellungen setzen. In diesem Fall werden nur noch die neusten Versionen des Samba Protokolls unterstützt.

Package Manager Mirror konfigurieren

Normalerweise, wenn das Kali System mit apt geupdated wird, kommt eine Verbindung zu einem jeweiligen Community Mirror in der Nähe zustande. Es ist aber schon seit langem möglich, Packages von CloudFlare CDN herunterzuladen. Mit kali-tweaks kann nun zwischen den Community Mirrors und der Cloudflare CDN gewechselt werden.

Kaboxer Themen Unterstützung

Mit dem neusten Kaboxer Update, sieht jenes nicht mehr altbacken aus, da es Unterstützung für die Windows Themen und Icon Themen anbietet. Somit können Programme sich an die Desktopumgebung orientieren und verhindern eher unschöne fallback Themen. Als Beispiel Zenmap mit dem Standard Kali Dark Themen und dem alten Erscheinungsbild.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/images/kaboxer-theme-support.png

Neue Tools

Es wäre kein Kali Release, wenn es nicht auch neue Tools gibt:

  • Dufflebag – Durchsuchen von Elastic Block Storage(EBS)
  • Mayam – Open-source Intelligence (OSINT) Framework
  • Name-That-Hash – Hashtyp ermitteln
  • Proxmark3 – RFID hacking
  • Reverse Proxy Grapher – Veranschaulichung des Reverse Proxy Flows
  • S3Scanner – Scannt offenen S3 buckets
  • Spraykatz – Benutzerdatenermittlung für Windows Maschinen und große AD Umgebungen
  • truffleHog – Durchsuchen von git Repositories nach String und anderen Secrets
  • Web of trust grapher (wotmate) – implementiert den veralteten PGP-Pfadfinder neu

Desktop & Themen Erweiterungen

Dieses Release bringt Neuerungen zu allen 3 Desktops, die sind Xfce, GNOME und KDE. Ein Update kommt für alle Desktops und das ist das neue Windows Button Design.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/images/new-window-buttons.png
Xfce

Für Xfce gibt es etwas mehr horizontalen Platz, sodass 2 neue Widges: CPU Usage und VPN IP, Platz finden. VPN IP bleibe versteckt, solange es keine VPN Verbindung gibt. Zusätzlich hat sich der Taskmanager verändert und zeigt jetzt nur noch “icons only” an. Die Panelhöhe wurde ebenfalls leicht erhöht.
Für das Terminal Dropdown Menu steht eine Powershell Verknüpfung zur Verfügung. Ihr könnt nun zwischen dem regulären Terminal, dem Root Terminal und Powershell wählen.

GNOME 41

GNOME hat indes 2 Versionssprünge gemacht. Seit dem letzten großen GNOME Update in Kali ist seither 1 Jahr vergangen. GNOME 40 und GNOME41 sind in der Zwischenzeit released worden. Alle Themen und Erweiterungen unterstützen die neue Shell.

KDE 5.23

Das KDE Team hat dieses Jahr ihr 25 Jähriges Bestehen gefeiert und veröffentlichten die 5.23 vom Plasma Desktop. Dieses Update ist nun in Kali verfügbar und bringt ein neues Design für das Breeze Themen mit.

Das Kali Themen update

All diese Themenänderungen werden nicht automatisch bei einem Kali Upgrade aktiviert. Dies ist aufgrund dessen, dass die Themen Settings unter dem Home Ordner des Users gespeichert sind, als dieser erzeugt wurde. Bei einem Kali upgrade wird das Betriebssystem upgedated aber nicht die persönlichen Dateien. Um die neuen Themen zu erhalten musst du entweder

  • Kali neu installieren
  • einen neuen User erzeugen und mit dem weiterarbeiten
  • das Desktopumgebungsprofil für den Benutzer löschen und ein reboot veranlassen. Hier ein Beispiel anhand von Xfce
kali@kali:~$ mv ~/.config/xfce4{,-$(date +%Y.%m.%d-%H.%M.%S)}
kali@kali:~$
kali@kali:~$ cp -rbi /etc/skel/. ~/
kali@kali:~$
kali@kali:~$ xfce4-session-logout --reboot --fast

Kali NetHunter Neuerungen

Für die Kali NetHunter App steht mit der 2021.4 ein Social-Engineer Toolkit zur Verfügung. Es können nun eigene Facebook, Messenger oder Twitter Emails versendet werden.

Kali ARM Neuerungen

  • Alle Images benutzen nun ext4 für deren root Dateisystem.
  • Support für den Raspberry Pi Zero 2 W. Wie bei dem Raspberry Pi 400 gibt es keine Nexmon Unterstützung
  • Raspberry Pi Images unterstützen nun out of the box das Booten vom USB, da das root device nicht länger hardgecoded ist.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/ 

Das Update

Um eine bestehende Installation auf 2021.4 upzudaten, führt folgende Befehle aus

sudo apt update
sudo apt -y full-upgrade
sudo reboot

mit dem nächsten Befehl könnt ihr überprüfen, ob die Version 2021.4 installiert ist

cat /etc/os-release 

PRETTY_NAME="Kali GNU/Linux Rolling"
NAME="Kali GNU/Linux"
ID=kali
VERSION="2021.4"
VERSION_ID="2021.4"
VERSION_CODENAME="kali-rolling"
ID_LIKE=debian
ANSI_COLOR="1;31"
HOME_URL="https://www.kali.org/"
SUPPORT_URL="https://forums.kali.org/"
BUG_REPORT_URL="https://bugs.kali.org/"

Canon Autofokus Betriebsarten

Kameras sind mit sogenannten Autofokus-Sensoren ausgestattet. Eher höherwertige Kameras haben den kompletten Bildbereich mit AF-Sensoren ausgestattet. Meine Canon EOS 6D Mark II hat insgesamt 45 Autofokus-Felder, die sich mehr Richtung Mitte befinden und sind nicht auf dem kompletten Bildbereich verteilt. Es gibt unterschiedliche Canon Autofokus Betriebsarten, die ich hier kurz vorstellen möchte. Einstellen könnt ihr diese mit dem AF Button oben auf dem Kamera Body. Folgendes Menu sollte sich öffnen.

ONE-SHOT AF

Der ONE-SHOT AF eignet sich am Besten für statische Objekte, die sich nicht weiter bewegen. Der ermittelte Schärfewert wird zwischen dem Antippen des Auslösers und dem nachfolgenden Auslösen nicht mehr verändert. Alle Objekte, die sich nach dieser Phase weiter fortbewegen, stellen die Gefahr dar, unscharf abgelichtet zu werden.

AI Servo AF

Bei AI Servo AF wird die Schärfe nachgezogen, sollte sich das Objekt bewegen. Die Kamera berechnet die Bewegung dann vor und ermöglicht so eine schnelle Fokussierung. Sollte das Objekt sich aus einem gerade verwendeten Messfeld bewegen, so wird es an das jeweils benachbarte Messfeld weitergegeben. Damit die Kamera die Schärfe fälschlicherweise nicht auf den Hintergrund legt, können die Messfelder begrenzt werden, sodass nicht alle benutzt werden. Dies hat den Vorteil, dass die Kamera das Bild nicht an ungewünschten Stellen scharf stellt. Das Motiv sollte dementsprechend im Bereich der Messfelder liegen. Sollte dies nicht gelingen, wäre eine größere Messzone von Vorteil, falls dies noch möglich ist.

AI Focus AF

Der AI Focus ist eine Art Kombinationen aus dem ONE-SHOT AF und dem AI Servo AF und bietet die Vorteile aus beiden Arten. Steht das zu fokussierende Objekt still, verwendet die Kamera ONE-SHOT AF. Ist es in Bewegung, so schaltet die Kamera automatisch auf AI Servo AF um und führt die Schärfe nach.

Vor allem bei bewegten Objekten kann es nützlich sein, die Serienbildfunktion zu benutzen. Die Wahrscheinlichkeit eines unscharfen Fotos ist sehr hoch, wenn Sie sich nur auf ein Foto verlassen.

Wichtig sind bei allen 3 Modis die Messfelder. Wie oben schon erwähnt hat die Canon 6d Mark II insgesamt 45 dieser Felder. Die Felder könnt ihr einzeln auswählen oder in eine Zone einteilen. Drückt dafür auf dem Body die entsprechende Taste, um in das Menu zu gelangen.

Im Menu könnt ihr eure gewünschten Einstellungen vornehmen. Je nach Anwendungsgebiet solltet ihr hier wählen.

Die Canon Autofokus Betriebsarten verwendet dann jene Felder für die Scharstellung.

Offline Sprachassistent mit Python

Ich habe schon immer mit einem Sprachassistenten geliebäugelt. Der teils vorhandene onlinezwang sowie die Erforderlichkeit den Sprachassistenten mit google, amazon, etc. zu verbinden, haben mich davor abgehalten einen zu ordern. Jetzt habe ich mir einen eigenen offline Sprachassistent mit Python erstellt. Als Spracherkennungsbibliothek verwende ich vosk. Vosk unterstützt mehr als 20 Sprachen, darunter auch deutsch und englisch. Die Spracherkennung funktioniert offline und sogar auf lightweight devices wie den Raspberry Pi.

Zur Einrichtung benötigen wir ein auf deutsch trainiertes Model. Von Vosk werden welche bereitgestellt, ihr könnt aber auch eigene Modelle erstellen.

Installation und Einrichtung

Die Installation von Vosk erledigen wir mit dem pip Manager. Für die Integration unseres Mikrophones ist sounddevice erforderlich. Zum Testen unserer Geräte installieren wir die alsa-utils, die eine Palette an Programme bereitstellt, unteranderem arecord und aplay.

# Abhängigkeiten installieren
sudo apt install python3-pyaudio alsa-utils libgfortran3

pip3 install vosk
pip3 install sounddevice

Wir laden uns zuerst das github von vosk herunter. Dort sind ebenfalls einige Beispiel Skripte enthalten. Geht in das Beispiel Verzeichnis und ladet ein Model eurer Wahl, um so vosk die Fähigkeit zu geben, unser Gesprochenes in Text umzuwandeln. Wichtig ist, dass das geladene und entpackte Model-Verzeichnis nach model umbenannt wird. Alle verfügbaren Models findet ihr hier: https://alphacephei.com/vosk/models

git clone https://github.com/alphacep/vosk-api

cd vosk-api/python/example
wget https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip
unzip vosk-model-de-0.21.zip
mv vosk-model-de-0.21/ model

Falls ihr noch kein Benutzer der Gruppe audio seid, fügt euch bitte hinzu. Ansonsten fehlen euch unter umständen die Berechtigungen, Aufnahme- und Ausgabegeräte zu benutzen.

development:~# grep audio /etc/group
audio:x:29:<user>

sudo reboot

Sounddevice/Mikrofon testen

Als Sounddevice kommt ein USB Mikrofon zum Einsatz. Bei Amazon habe ich mir das Tyasoleil USB Mikrofon gekauft, und muss sagen, dass ich mehr als beeindruckt bin. Es kann mich im kompletten Raum aufnehmen. Ich habe es hier verlinkt: Mikrofone*. Mit arecord können wir die verfügbaren Aufnahmegeräte auflisten.

arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: I82801AAICH [Intel 82801AA-ICH], device 0: Intel ICH [Intel 82801AA-ICH]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 0: I82801AAICH [Intel 82801AA-ICH], device 1: Intel ICH - MIC ADC [Intel 82801AA-ICH - MIC ADC]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

Ebenfalls bietet es uns die Möglichkeit eine Aufnahme zu starten und später als .wav Datei zu speichern.

arecord -f S16_LE -d 10 -r 16000 ./test-mic.wav
Recording WAVE './test-mic.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Mono

Mit aplay können wir unsere gerade erstellte .wav Datei anhören und so die Tests des Mikrofons abschließen.

aplay test-mic.wav
Playing WAVE 'test-mic.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Mono

Python3 Code

Zuerst laden wir die benötigten Python Bibliotheken. und erstellen eine Queue. Queue ist eine Art lineare Datenstruktur und arbeitet nach dem FIFO Prinzip. FIFO stammt aus dem englischen und bedeutet First-In-First-Out. Kurz gesagt, was zuerst reinkommt, geht auch als erstes wieder raus. Wir haben einen zusätzlichen Thread, der die Aktivphase managed und für 10 Sekunden offen hält. Dazu aber später mehr.

import argparse
import os
import queue
import sys
import json
import sounddevice as sd
import vosk
import threading
import time
import gpiozero

q = queue.Queue()

Activities Klasse

In der Activities Klasse sind alle Aktivitäten aufgelistet, die wir benutzen können. Zurzeit beschränken wir uns auf Licht an/aus und Tor an/aus. In beiden Fällen triggern wir eine LED, die mit unterschiedlichen GPIO gesteuert wird.

Speech Klasse

Die Speech Klasse ist die Hauptklasse. Hier wird der Startcode definiert, auf dem unsere Sprachsteuerung reagiert und nach dem wir unsere Kommandos sagen können. Der Startcode ist ähnlich wie “alexa” bei Amazon. Wurde der Startcode herausgehört, haben wir ein 10 Sekunden Fenster für die Kommandos, ehe wir unseren Startcode erneut sagen müssen. Während unser Sprachfenster offen ist, leuchtet zusätzlich noch eine grüne LED. Die 10 Sekunden Zählschleife, unsere Aktivphase, ist in einem Thread gepackt und läuft im Hintergrund. Ebenso wie das an und ausschalten der grünen LED. Das hat den Vorteil, dass unser Programm weiterhin auf das Gesprochene reagieren kann, während es die Zeit herunterzählt. Solange unser Thread läuft, können wir die Activities Methoden mit unserer Sprache steuern.

class Activities:
# Definiere die Aktivitäten, die mit deiner Sprache gesteuert werden können.

    LICHT_LED = LED_TOR = None
    
    def licht(GPIO):
        # Schalte das Licht an und aus
        print(f" Schalte das Licht an/aus mit {GPIO}")
        if Activities.LICHT_LED is None:
            Activities.LICHT_LED = gpiozero.LED(GPIO)
        try:
            Activities.LICHT_LED.toggle()
            
        # if any error occurs call exception
        except gpiozero.GPIOZeroError as err:
            print("Error occured: {0}".format(err))
    
    def tor(GPIO):
        # zur Demonstrationszwecken wird hier nun eine Ausgabe definiert. 
        print(f" Schalte das Tor an mit {GPIO}")


class speech: 
    
    STARTCODE = 'computer'
    def __init__(self,startcode):
        self.STARTCODE = startcode

    # Unsere Thread Funktion
    def thread_timer(self):
        # Aktiviere GPIO 17, um die grüne LED zum Leuchten zu bringen. 
        led = gpiozero.LED(17)
        self.power_gpio(17,led)
        # warte 10 Sekunden
        time.sleep(10)
        # Schalte die grüne LED wieder aus.
        self.close_gpio(17,led)
        
    # Definieren der Aktivierungsphase. Solange der thread gestartet ist, können Kommandos zum triggern der Methoden aus der Activities Klasse gesagt werden.
    # 
    def active(self,rec):
        print("active")
        # Thread definieren
        t = threading.Thread(target=self.thread_timer)
        # Thread starten
        t.start()
        i=0
        # solange Thread aktiv
        while t.is_alive():
            print('call a command')
            # hole die Daten aus der Queue, bzw. aus dem Stream
            data = q.get()
            if rec.AcceptWaveform(data):
                print("second record")
                # 
                res = json.loads(rec.Result())
                if 'LICHT'.upper() in res['text'].upper():
                    Activities.licht(18)

                elif 'Tor'.upper() in res['text'].upper():
                    Activities.tor(18)
                print(res['text'])


    def power_gpio(self,GPIO,led):
        print(f"Power {GPIO}")
        try:
            # switch LED on
            if not led.is_lit:
                led.on()
        # if any error occurs call exception
        except gpiozero.GPIOZeroError as err:
            print("Error occured: {0}".format(err))
    
    def close_gpio(self,GPIO,led):
        print(f"Close {GPIO}")
        try:
            # switch LED off
            if led.is_lit:
                led.off()
        # if any error occurs call exception
        except gpiozero.GPIOZeroError as err:
            print("Error occured: {0}".format(err))

Main

In der Main parsen wir die Argumente, die wir dem Programm übergeben können. Falls keine angegeben worden sind, übernimmt das Script definierte Standardwerte. Wir überprüfen, ob der Ordner model existiert und erstellen ein Objekt der Klasse speech mit unserem definierten Aktivierungswort. Unsere Sprache muss natürlich noch aufgezeichnet werden. Dies machen wir mit der Methode RawInputStream der sounddevice Klasse. Wichtig hier ist vor allem der callback Parameter vom Typ “callable”. Diesem geben wir unsere gleichnamige Funktion über, die folgenden Aufbau benötigt:

callback(indata: buffer, frames: int,
         time: CData, status: CallbackFlags) -> None

In dieser Funktion geben wir unserem Sprach-Stream, den wir zuvor mit RawInputStream eingefangen haben, als Bytes in die am Anfang definierte Queue. In unserer while Schleife entnehmen wir unserer Queue die Daten und übergeben diese der Methode AcceptWaveForm. Die gerade genannte Methode versucht die gesprochenen Sätze zu erkennen. Erst wenn es das Ende vermutet, gibt AcceptWaveForm True zurück. In rec.Result() befindet sich das Ergebnis des gesprochenen Textes. Wird erhalten ein String, den wir mit json.loads als JSON parsen und weiterverarbeiten können. Wird da Aktivierungswort herausgehört, geht es mit dem gleichen Vorgehen im Aktivierungsfenster weiter.

def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
        pass
    q.put(bytes(indata))

if __name__ == '__main__':
    parser = argparse.ArgumentParser(add_help=True)
    parser.add_argument(
        '-m','--model', type=str, nargs='?',default='model', help='Pfad zum Model'
    )
    parser.add_argument(
        '-d','--device', type=str,nargs='?',default='1,0',help='Eingabegerät(Mikrofon als String)'
    )
    parser.add_argument(
        '-r','--samplerate',type=int,nargs='?', default=44100,help='Sample Rate'
    )

    args = parser.parse_args('')

    if not os.path.exists(args.model):
        print("Please download a model from https://alphacephei.com/vosk/models and unpack to 'model'")
        #exit(1)

    model = vosk.Model(args.model)
    # Speech Objekt erstellen und Übergabe des Aktivierungsworts
    speech = speech('computer')
    # 
    with sd.RawInputStream(samplerate=args.samplerate, blocksize=8000, device=None,dtype='int16',
                            channels=1, callback=callback):
        print('*' * 80)
        # Aktivierung der vosk Spracherkennung mit Übergabe des geladenen Models. Übersetze das Gesprochene in Text.
        rec = vosk.KaldiRecognizer(model, args.samplerate)
        while True:
            # Daten aus der Queue ziehen
            data = q.get()
            print("start to speak")
            if rec.AcceptWaveform(data):
                # erhalte das erkannte gesprochene als String zurück
                x = rec.Result()
                print(x)
                print(rec.Result())
                # wandelt den String in Json um
                res = json.loads(x)
                print(res)
                # wenn der Aktivierungscode herausgehört wurde, wird die active Methode von Speech gestartet
                if speech.STARTCODE == res['text']:
                    speech.active(rec)

            else:
                pass

Aufbau der Schaltung

Vom Prinzip her sind es zwei die gleichen Schaltungen. Nur der benutzte GPIO Pin ist ein anderer. Beide LEDs sind mit einem 330 Ohm Wiederstand geschaltet. Der Rest kann in der Schaltung begutachtet werden.

Den kompletten Code findet ihr bei github.

Update: Ich habe den Sprachassistenten nun mit einem LED-Streifen verbunden. Den Beitrag findet ihr hier: LED-Streifen mit offline Sprachassistenten steuern