Unser schönes Emsland

Heute möchte ich euch ein kleines Herzens Projekt vorstellen, bei dem ich beides vereint habe. Meine Leidenschaft fürs Fotografieren, als auch die Leidenschaft für die Informatik. Wie ihr im Namen seht, geht es um den Landkreis, in dem ich wohne. Ich bin der Meinung, auch hier im Emsland, haben wir viele Sehenswürdigkeiten, auch wenn diese nicht auf den ersten Blick ersichtlich sind. Ich versuche sowohl bekannte als auch unbekannte sehenswürdige Orte mit einzubringen. Darunter die offensichtlichen wie z.B. das Schloss Clemenswerth, aber auch solche, die wahrscheinlich noch niemand abgelichtet hat. Das Emsland besteht nämlich nicht nur aus seinen bekannten Destillerien 😉

In diesem Frame könnt ihr die Marker sehen. Die Marker stehen jeweils für ein sehenswerten Ort.

Mit dem Bild und dem Standort kommen noch kleine Informationen über den Ort/Sehenswürdigkeit, falls es denn soweit welche gibt. Darunter versuche ich noch allgemeine Informationen mit einzubringen, ob z.B. der Ort barrierefrei ist, wie dieser am besten zu erreichen ist (Auto, Bus, zu Fuß), ob es dort Verpflegung gibt und Parkmöglichkeiten vorhanden sind. Die Karte kann als Vollbild über https://steviesblog.de/blog/map aufgerufen werden.

Bitte achtet drauf die Orte sauber zu verlassen, keine Gegenstände/Pflanzen zu zerstören und die Tierwelt nicht zu stören! Respektiert auch bitte den Privatbesitz.

Technische Details

Ich möchte euch auch nicht vorhalten, wie genau dies funktioniert. Zuerst, wenn ich ein Spot erreicht habe, brauche ich die GPS Koordinaten. Dafür habe ich mir eine kleine App mit Kotlin geschrieben, die mir diese in eine CSV Datei abspeichert, mit dem Namen, den ich vorher vergeben habe. Diese Informationen speichere ich dann später in der Datenbank ab. Dafür existiert eine eigenständige Tabelle.

DROP TABLE IF EXISTS `c_spot`;

CREATE TABLE `c_spot` (
  `ID` int NOT NULL AUTO_INCREMENT,
  `NAME` varchar(45) NOT NULL,
  `LATITUDE` decimal(24,15) NOT NULL,
  `LONGITUDE` decimal(24,15) NOT NULL,
  `MARKER_ID` INT(11) NOT NULL,
  `PICTURE_ID` bigint unsigned NOT NULL,
  `TEXT` varchar(245) NOT NULL,
  `DATE` datetime NOT NULL,
  `CREATED_AT` datetime NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `SPOT_UNIQUE` (`NAME`,`LATITUDE`,`LONGITUDE`),
  KEY `PICTURE_ID` (`PICTURE_ID`),
  KEY `MARKER_ID` (`MARKER_ID`),
  CONSTRAINT `ibfk_1` FOREIGN KEY (`PICTURE_ID`) REFERENCES `posts` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
  CONSTRAINT `ibfk_2` FOREIGN KEY (`MARKER_ID`) REFERENCES `c_spot_marker` (`id`)
)
DROP TABLE IF EXISTS `c_spot_marker`;

CREATE TABLE `c_spot_marker`(
  `ID` INT(11) NOT NULL AUTO_INCREMENT,
  `NAME` VARCHAR(50) NOT NULL,
  PRIMARY KEY (`ID`)
)

Die Daten gelangen über ein Plugin in die Tabelle. Die Spalte „TEXT“ enthält den Html Code für das Popup-Fenster mit den zusätzlichen Informationen. Leaflet wird benutzt, um die Makierer zu platzieren, während OpenStreetMap(OSM) als Karte dazu gezogen wird. Eine for-Schleife geht über alle definierten Punkte in der Tabelle und platziert diese auf der Karte.

Cursor in Mysql

Ein Cursor in Mysql hilft euch über ein Result Set zu iterieren. Dabei muss er vorher definiert und geöffnet werden. Beim Deklarieren geben wir ihm das Select Statement mit, über das er iterieren soll. Der Cursor geht durch jede einzelne Zeile des Result Sets, bis er zum Ende angekommen ist. Dort schließen wir den Cursor wieder. Das Verhalten werde ich später im Beispiel genauer erklären.

Eigenschaften eines Cursors

Mysql unterstützt Cursors in z.B. stored Procedures. Folgende Eigenschaften sind zu beachten:

  • Read only: es sind soweit keine Updates/Inserts direkt mit dem Cursor möglich
  • Ein Cursor arbeitet sich von oben nach unten durch und kann keine Zeile überspringen
  • Er muss vorher deklariert werden

Cursor Beispiel

Das Beispiel, welches ich hier mit einbeziehe, ist ein echtes Beispiel, welches von mir benutzt wird. Für das Windparc-Dashboard benötige ich am Anfang eines jeden Jahres die Plan-Daten, die ich per PDF Datei zur Verfügung gestellt bekomme. Ich hatte versucht diesen Prozess zu automatisieren, scheitere allerdings daran, dass die Tabellen in der PDF nicht korrekt ausgelesen werden können. Ich muss also 24 Windmühlen x 12 Monate = 288 Zeilen in der Tabelle anlegen, die außer der Windmühlen-ID, Jahr, Monat, Plan- und Istwerte die gleichen Werte haben. Diesen Schritt wollte ich mir also vereinfachen und habe mir ein stored Procedure geschrieben. Das Jahr wird dabei als IN Parameter übergeben.

Zur besseren Beschreibung des Prozesses, habe ich die Schritte visuell dargestellt. Kurz beschrieben hole ich mir die Windmühlen ID. Pro ID wird jeder Monat als ein Datensatz in der Tabelle erzeugt.

Prozess Beschreibung

Cursor Prozess Kette

Erklärung des Codes

Kommen wir also zum ersten Schritt. Ich deklariere die Variablen im Code, die notwendig sind. Darunter sind „finished“, „mill“, „month“ und natürlich der Cursor selbst. Zu den 3 ersten Variablen folgt die Definition:

  • „finished“ ist ein Boolean und hilft uns später aus der Schleife zu springen.
  • „mill“ speichert die ID der Windmühle
  • „month“ ist die Anzahl der Monate, für die while Schleife
/*Wird für den Loop benötigt*/
DECLARE finished INTEGER DEFAULT FALSE;
/*Variable für die Mühlen*/
DECLARE mill int DEFAULT 0;
/*Um von Monat 12 herunter zu zählen*/
DECLARE month int DEFAULT 12;
/*Cursor deklarieren */    
DECLARE curwindmill CURSOR 
FOR select ID from DB.WIND_MILL where ID <> 1;
/*Wenn der Loop am Ende angekommen ist setze finished = TRUE um aus dem Loop zu springen*/
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = TRUE;

Wir deklarieren den Cursor und geben ein Select Statement mit, über das er iterieren soll. Der Continue Handler hilft uns die Variable finished auf TRUE zu setzen, sobald die letzte Zeile vom Dataset verarbeitet wurde. Damit ihr euch besser vorstellen könnt worüber wir iterieren, bilde ich die WIND_MILL Tabelle teils ab.

IDWINDMILLLOCATIONTYPEWINDPARCCREATED_ATUPDATED_AT
2H-WEA0122108.06.2020 20:5608.06.2020 20:56
3H-WEA0222108.06.2020 20:5608.06.2020 20:56
4H-WEA0322108.06.2020 20:5608.06.2020 20:56
6H-WEA0522108.06.2020 20:5608.06.2020 20:56
24G-WEA0544108.06.2020 20:5608.06.2020 20:56

Durch die Deklarierung können wir nun den Cursor öffnen und ein Loop erzeugen. get_loop ist dabei nur der Loop Name.

/* Cursor öffnen*/
OPEN curwindmill;
/*Über jeden Eintrag loopen*/
get_loop: LOOP

Mit Fetch holen wir uns die Daten vom Cursor und schreiben den Wert in mill. Sollte der Cursor am Ende angekommen sein, so wird der Loop anhand finished = True verlassen.

/*Das Ergebnis aus dem Cursor in mill schreiben*/
FETCH curwindmill INTO mill;
/*Sollte finished True werden, was der Fall ist, wenn über jeden Eintrag iteriert wurde, dann verlasse den Loop*/
IF finished THEN
    /*Loop verlassen*/
    LEAVE get_loop;
END IF;

Da ich hier noch die Monate beachten muss, habe ich eine while Schleife mit eingebaut. Diese sorgt dafür, dass von 12 heruntergezählt wird. Pro Monat wird für die ID ein neuer Datensatz erzeugt. Durch das IGNORE werden eventuell auftretende Fehler im Insert ignoriert und der Cursor springt zum nächsten Datensatz ohne dass ein neuer Datensatz angelegt wurde.

/*Solange month > 0 is schreibe die Daten in die Dabelle. Sollte der Eintrag schon vorhanden sein, ignoriere und gehe zum nächsten*/
WHILE month > 0 DO
    /*Datensatz einfügen*/
    INSERT IGNORE DB.WIND_ENERGY_PERFORMANCE VALUES(mill,month, YEAR, NULL, 0 , 1, 0, now(), now() );
    /*Monat subtrahieren*/
    SET month = month - 1;
END WHILE;
/*month resetten und für die nächste Mill von vorne anfangen*/
SET month = 12;

Zum Schluss wird die Variable month noch resetted und die while Schleife beendet, beziehungsweise erneut aufgerufen. Sollte das Ende erreicht sein, wird der Cursor geschlossen

SET month = 12;
END LOOP get_loop;
/*Cursor schließen*/
CLOSE curwindmill;
END

Stored Procedure ausführen

Mit einem CALL, gefolgt vom Procedure Name und dem Jahr können wir nun neue Zeilen generieren.

call Metabase.P_WIND_ENERGY_PERFORMANCE_ADD_YEAR(2021)
WINDMILLMONTHYEARISPERPLANPERFUNITISPERF(PCT)CREATED_ATUPDATED_AT
312021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
322021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
332021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
342021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
352021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
362021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
372021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
382021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
392021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
3102021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
3112021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
3122021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
412021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
422021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
432021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
442021NULL01016.03.2021 20:01:2016.03.2021 20:01:20
24122021NULL01016.03.2021 20:01:2016.03.2021 20:01:20

Kompletter Code

CREATE DEFINER=`user`@`%` PROCEDURE `Name`(
	IN YEAR int
)
BEGIN
	/*Wird für den Loop benötigt*/
	DECLARE finished INTEGER DEFAULT FALSE;
    /*Variable für die Mühlen*/
    DECLARE mill int DEFAULT 0;
    /*Um von Monat 12 herunter zu zählen*/
    DECLARE month int DEFAULT 12;
    
        /*Cursor deklarieren*/
	DECLARE curwindmill CURSOR 
		FOR 
		select ID from DB.WIND_MILL where ID <> 1;
    /*Wenn der Loop am Ende angekommen ist setze finished = TRUE um aus dem Loop zu springen*/
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = TRUE;
    /* Cursor öffnen*/
	OPEN curwindmill;
    /*Über jeden Eintrag loopen*/
		get_loop: LOOP
        /*Das Ergebnis aus dem Cursor in mill schreiben*/
			FETCH curwindmill INTO mill;
            /*Sollte finished True werden, was der Fall ist, wenn über jeden Eintrag iteriert wurde, dann verlasse den Loop*/
            IF finished THEN
            /*Loop verlassen*/
				LEAVE get_loop;
			END IF;
            /*Solange month > 0 is schreibe die Daten in die Dabelle. Sollte der Eintrag schon vorhanden sein, ignoriere und gehe zum nächsten*/
            WHILE month > 0 DO
            /*Datensatz einfügen*/
				INSERT IGNORE DB.WIND_ENERGY_PERFORMANCE VALUES(mill,month, YEAR, NULL, 0 , 1, 0, now(), now() );
                /*Monat subtrahieren*/
                SET month = month - 1;
			END WHILE;
		/*month resetten und für die nächste Mill von vorne anfangen*/
		SET month = 12;
		END LOOP get_loop;
        /*Cursor schließen*/
    CLOSE curwindmill;
END

Was ist arp spoofing

Beim Arp-Spoofing werden gefälschte ARP-Replys versendet und die Zuordnung zwischen Arp und IP manipuliert. Möchte z.B. Rechner A eine Nachricht an Rechner B schicken, ermittelt dieser vorerst die zugehörige MAC-Adresse zu der IP von Rechner B. Rechner A schickt dabei ein Broadcast an alle Geräte. Vereinfacht gesagt enthält dieser Broadcast die MAC+IP Adresse des Fragenden und verlangt die korrekte MAC Adresse zur IP von Rechner B. Anbei eine Visualisierung der ARP Pakete in Wireshark.

arp spoofing netzwerkverkehr

Alle Rechner im Netzwerk speichern sich die IP-MAC Relation des Fragenden. Nur der Rechner B antwortet Rechner A mit seiner MAC-Adresse. Die Antwort speichert Rechner A dann in seine lokale Tabelle, dem ARP-Cache.
ARP-Spoofing macht es sich zu Nutze, dass die erste Antwort, die der Rechner A bekommt, akzeptiert und gespeichert wird. Der Man-in-the-Middle PC versucht den korrekten Rechner B zuvorzukommen, damit Rechner A die MAC vom MITM-PC mit der IP von Rechner B abspeichert.

arp spoofing schaubild

Jeglicher Netzwerk Traffic, der von Rechner A nach Rechner B geht, wird nun über den PC des Angreifers geleitet. Dieser muss lediglich dafür sorgen, dass er die IP Pakete weiterleitet, um so kein misstrauen zu erzeugen. Die Route zurück von Rechner B nach Rechner A sollte auch nicht missachtet werden.

Beispiel

Bei meinem QNAP Test habe ich mir diese Attacke zu Nutze gemacht. Durch die Manipulation des ARP Caches meines NAS konnte ich den ausgehenden Traffic mitschneiden. Ich möchte euch hier zeigen, wie ich vorgegangen bin. Vorerst wäre es interessant zu wissen, welche ARP Tabelle zurzeit existiert. Dies ist die vom NAS. Weiter unten zeige ich euch die Liste nach der erfolgreichen Attacke erneut.

ip neigh show
192.168.188.1 dev eth0 lladdr e0:28:6d:c4:04:5a DELAY

Bitte beachtet, dass es sich hierbei um einen Angriff handelt, den ihr nur bei euren eigenen Geräten ausprobieren solltet. Andernfalls kann dies strafrechtlich verfolgt werden!

Sorgen wir erst einmal dafür, dass unser Laptop alle Pakete weiterleitet.

sudo sysctl -w net.ipv4.ip_forward=1

Kommen wir nun zum ARP-Reply, den wir manipulieren. Dafür brauchen wir unseren Schnittstellennamen, der zum Netzwerk verbunden ist.

ip addr

Bei mir ist das eth0, kann bei euch aber abweichen. Wie wir oben gelernt haben, wollen wir unser MAC-Adresse zu einer anderen IP Adresse auflösen lassen. In meinem Fall die IP von meinen Router und vom NAS. Dafür benötigt es die IP des Routers und des NAS, die wir in unterschiedlichen Terminals als Target angeben.

# Router 192.168.188.1
# NAS 192.168.188.140

sudo arpspoof -i <Schnittstelle> -t <Opfer> <manipulierte IP>
sudo arpspoof -i eth0 -t 192.168.188.1 192.168.188.140
sudo arpspoof -i eth0 -t 192.168.188.140 192.168.188.1

Der Angriff erfolgt. Die beiden Befehle werden ihr Ergebnis fortlaufend im jeweiligen Terminal niederschreiben. Nach einem Blick in den ARP-Cache sehen wir, dass der Angriff erfolgreich war.

[~] # ip neigh show
192.168.188.83 dev eth0 lladdr 08:00:27:74:01:0d REACHABLE # Angreifer
192.168.188.1 dev eth0 lladdr 08:00:27:74:01:0d REACHABLE # Router

Sogenannte Sniffer können nun den Traffic mitlesen. Bekannte Sniffer sind z.B. Wireshark, urlsnarf, driftnet und viele mehr. Nachdem ihr fertig seid, solltet ihr die Paketweiterleitung von oben deaktivieren

sudo sysctl -w net.ipv4.ip_forward=0

QNAP dubiose Verbindungen

Bei einer Kontrolle des QNAP TS-212 bin ich auf dubiose Verbindungen gestoßen. Generell sind Verbindungen ja nichts ungewöhnliches auf einem NAS. Allerdings haben mich die ausgehenden Verbindungen skeptisch gemacht. Immer wieder tauchten dort eine Vielzahl von UDP Paketen auf. Mittels einem Man-in-the-middle Angriff hatte ich mich zwischen den NAS geschaltet. Wireshark habe ich benutzt, um den Netzwerkverkehr aufzulisten. Mit dem Filter

ip.addr == 192.168.188.140 && ((udp.port == 6889 || udp.port == 6881 ))

wurden mir nur die gewünschten Pakete angezeigt. Mir war bei den Verbindungen aufgefallen, das es sich immer um die Ports 6889 + 6881 handelt, daher habe ich den Filter auf die 2 Ports beschränkt.

Einige der Zielquellen habe ich überprüft. Dabei fand ich heraus, dass es sich um Verbindungen nach Somalia, Belarus, etc. handelt. Mir war nicht bewusst welcher Service auf dem NAS eigenständig Verbindungen nach genannten Orten aufbaut und ging schon vom Schlimmsten aus. Nach einer kurzen Recherche fand ich heraus, dass die Ports 6889 + 6881 zu Bittorrent gehören. Beruhigt hatte mich das vorerst nicht. Nach weiterer Recherche habe ich die Download-Station von QNAP als den Übeltäter ermittelt.

Download Station ist ein webbasiertes Download-Werkzeug, mit dem Sie Dateien aus dem Internet über BT, PT, Magnet-Link, HTTP/HTTPS, FTP/FTPS, Xunlei, FlashGet, qqdl und Baidu Cloud-Downloads herunterladen sowie RSS-Feeds abonnieren können. Mit der Funktion BT-Suche können Sie BT-Seeds einfach aufspüren und herunterladen und Ihr QNAP NAS in ein 24/7-Download-Center verwandeln.

https://docs.qnap.com/nas-outdated/4.2/SMB/de/index.html?download_station.htm

BT(Bittorrent)
Verbindungseinstellung:
Geben Sie die Ports für BT-Downloads an. Die Standardportnummern sind 6881 – 6889.

https://docs.qnap.com/nas-outdated/4.2/SMB/de/index.html?download_station.htm

Die Download-Station ist eine von mir nicht benutzte App, sodass ich diese ohne weiteres deinstalliert habe. Nach der Deinstallation hörten auch schon die dubiosen Verbindungen auf.

Randnotiz

Ich konnte die Download Station Verbindungen leider im Nachhinein nicht über netstat bzw. lsof festellen. Beide zeigten mir zwar Verbindungen an, aber nicht die, die ich über den MITM-Angriff sehen konnte. Dabei habe ich folgendes ausprobiert

while :; do netstat -uan | egrep '6881|6889' >> /share/Public/watch.log; done
netstat: udp        0      0 0.0.0.0:6889            0.0.0.0:*


# 18942 ist die PID vom Download Manager.
while :; do lsof -Pp 18942 | grep IPv4 >> /share/Public/watch.log; done
dsd     18942 admin   14u     IPv4    3949996      0t0       TCP *:6889 (LISTEN)
dsd     18942 admin   15u     IPv4    3949997      0t0       TCP *:4433 (LISTEN)
dsd     18942 admin   16u     IPv4    3950002      0t0       UDP *:6889
dsd     18942 admin   26u     IPv4    3950017      0t0       UDP *:6771
dsd     18942 admin   27u     IPv4    3950018      0t0       UDP localhost:6771
dsd     18942 admin   28u     IPv4    3950019      0t0       UDP localhost:46161
dsd     18942 admin   29u     IPv4    3950020      0t0       UDP NA:6771
dsd     18942 admin   30u     IPv4    3950021      0t0       UDP NAS:50750
dsd     18942 admin   31u     IPv4    3950024      0t0       UDP *:1900
dsd     18942 admin   32u     IPv4    3950025      0t0       UDP localhost:1900
dsd     18942 admin   33u     IPv4    3950026      0t0       UDP localhost:35034
dsd     18942 admin   34u     IPv4    3950027      0t0       UDP NAS:1900
dsd     18942 admin   35u     IPv4    3950028      0t0       UDP NAS:38742
dsd     18942 admin   37u     IPv4    3950056      0t0       UDP *:42835


# Die PID habe ich mit ps herausgefunden. dsd ist der Service für den Download Manager 
ps -aux | grep dsd
12004 admin       568 S   grep dsd
18942 admin      4260 S   /usr/sbin/dsd -level error -log /share/MD0_DATA/.qpkg/DSv3/log/dsd.log

Ich habe dabei ein while Endlosschleife benutzt, damit mir dauerhaft die Ergebnisse in die watch.log geschrieben werden. Zum erwünschten Ergebnis führte dies leider nicht.

Image Crawler Python

Möchte man sein Machine Learning Model eigenständig trainieren, ist es teilweise hilfreich einige Bilder zur Verfügung zu haben. Lass uns einfach mal davon ausgehen, dass unser Neuronales Netzwerk eine Katze erkennen soll. So sollten wir tausende Bilder von Katzen aus dem Netz laden, um unser Model erfolgreich trainieren zu können. Die Katzen wären die positiven Ergebnisse und z.B. Hunde, Häuser, Kühe die negativen. Anhand dieser Konstellation zwischen positiven und negativen Bildern, kann unser Netzwerk die Katze klassifizieren. Dazu habe ich ein Python Skript geschrieben. Je nachdem was die Keywörter sind, sucht es passende Bilder. Die Bilder werden heruntergeladen, verkleinert und in Graustufe gespeichert. Ich habe jeweils ein Skript für die negativen Bilder und eins für die positiven. Die beiden Skripte unterscheiden sich nur in dem Pfad, wo die Bilder später abgelegt sind. Selbstverständlich können die Skripte auch für andere Verwendungszwecke dazu gezogen werden. Ich benutze das Package iCrawler, welches mir Methoden für die gängigen Suchmaschinen, wie Bing, Google, etc., zur Verfügung stellt.

pip install icrawler

Weiterhin verwende ich cv2, um das Bild im nachhinein zu verkleinern und es als Graustufe zu speichern. In diesem Blogbeitrag habe ich die Installation beschrieben: OpenCV auf dem Raspberry Pi installieren.

Der Code

Kommen wir nun zum eigentlichen Code. Dieser empfängt Parameter entgegen, die wir beim Aufruf übergeben. Für jedes Keyword, führt es ein Crawl aus, der nach den Bildern in der angegebenen Suchmaschine sucht. Die Bilder ermittelt iCrawler anhand bestimmter Werte. Zum einen ist das der Dateityp, der hier .jpg oder .png ist. Wir definieren ein Filter der nach der Größe ‚large‘ und der Lizenz ‚commercial, modify‘ gegenprüft. Nachdem die Bilder im Ordner gespeichert sind, geht der nächste Part über die Bilder, verkleinert und ergraut diese.

from icrawler.builtin import BingImageCrawler
import os
import cv2
import argparse

# Create Argument Parser
ap = argparse.ArgumentParser()
ap.add_argument("-k", "--keyword", required=True,help="keywords of the images to download separated by ',' ")
ap.add_argument("-m", "--max-number", required=True,help="declare max number")
args = vars(ap.parse_args())

# directory of loaded images
directory = 'img/pos/'

# keyword to search for
keyword=args["keyword"].split(',')
# allowed filetypes 
resize_file_types = ("jpg","png")

#Initiate bing_crawler and define threads + storage
bing_crawler = BingImageCrawler(
    feeder_threads=1,
    parser_threads=2,
    downloader_threads=4,
    storage={'root_dir':directory}
)

#create filter
filters = dict(
    size='large',
    license='commercial,modify',
)

# crawl bing for each keyword
for key in keyword:
    bing_crawler.crawl(keyword=key,filters=filters,max_num=int(args['max_number']),file_idx_offset=0)

# grayscale + resize each image found
message = "** resize and grayscale downloaded Image/s ({0})**".format(len(os.listdir(directory)))
print()
print("*"*int(len(message)))
print("*"*int(len(message)))
print(message)
print("*"*int(len(message)))
print("*"*int(len(message)))

# for each file in directory
for filename in os.listdir(directory):
    # if file is a picture
    if filename.endswith(resize_file_types):
        print("resize and grayscale {0}{1}".format(directory,filename))
        # make grayscale
        img = cv2.imread(directory + filename, cv2.IMREAD_GRAYSCALE)
        # resize image
        resized_img = cv2.resize(img, (100,100))
        # write new picture
        cv2.imwrite(directory + filename, resized_img)

message = "** found {0} Image/s **".format(len(os.listdir(directory)))
print()
print("*"*int(len(message)))
print("*"*int(len(message)))
print(message)
print("*"*int(len(message)))
print("*"*int(len(message)))

Um auf das Beispiel von oben mit den Katzen zurückzukommen. Der Aufruf sieht wie folgt aus:

python pos_img_crawler.py -k katze -m 1000

Und natürlich die negativen Bilder(Haus, Hund, Kuh). Achtet bitte darauf, dass es sich um zwei unterschiedliche Dateien handelt :

python neg_img_crawler.py -k haus,hund,kuh -m 1000

Raspberry räumlich managen

Schon lange war ich auf der Suche nach einer Möglichkeit meine 3 Raspberry PIs räumlich zu managen. Hintergrund war, dass ich die Raspberry PIs nicht immer, je nach Gebrauch, wieder rausholen wollte und diese bei längeren Benutzen auch meist im Weg rumlagen. Für jeden PI wurde noch eine Stromversorgung benötigt, sodass der Dreifachstecker meist schon durch die 3 PIs ausgelastet war. Alles in Allem ein schönes Chaos. Dann bin ich über ein offenes Cluster Gehäuse gestoßen, welches maximal 4 Stück beherbergen kann. Für knappe 40€ könnt ihr das Gehäuse von GeeekPi* erhalten. Das aus Akryl bestehende Gehäuse bietet Verschraubungen für den PI 1 + 2 + 3 + 4, 2.5″ HDD, 3.5″ HDD und weitere mehr. Ausgestattet ist es mit einem Lüfter, der über GPIOs des PIs mit Strom versorgt werden kann.

Mein Tipp: um die Schutzfolie von dem Acryl vernünftig abzubekommen, sollte es für ein paar Minuten eingeweicht werden.

Für das Kabelmanagement benutze ich ein Anker PowerPort Speed 5, 63W 5 Port USB* Ladegerät. Es bietet 2 Ports mit 3.0A, benötigt der PI 4 und 3 Ports mit jeweils max. 2.4A, passend für den PI 3 + 2 + 1. Es stemmt ebenfalls die Kamera + Lüfter, die an den PIs angeschlossen sind. Mittlerweile läuft dies seit einigen Wochen sehr stabil. Alle PIs sind dabei dauerhaft an.

Das nächste Gadget zum besseren Management betrifft nicht direkt das räumliche. Es ist ein Streaming Deck*, welches, wie der Name schon sagt, eigentlich für Streamer ist. Ich habe es aber „zweckentfremdet“ und benutze es für unterschiedliche Aufgaben, die ich darüber direkt starten kann. Über das Deck kann ich z.B. eine Putty Session zum PI direkt aufbauen. Durch den SSH-Key Austausch ist eine Passworteingabe nicht nötig.

Stream Deck

Diese 3 Sachen gepaart sind für mich ein unschlagbares Trio und ich möchte es nicht mehr missen. Alles sieht wesentlich geordneter aus und ich habe meine PIs auch immer Griffbereit. Er steht nicht, wie auf den Fotos zu sehen, auf dem Schreibtisch, sondern ist im Regal untergebracht 😉

So sichere ich meine Daten mittels Backup vor einem Datenausfall

Was ist das schlimmste was einem am PC, bzw. im System passieren kann? Richtig ein Datenausfall bzw. Verlust. Vor allem die Daten von „unschätzbaren“ Wert, wie. Fotos von Verstorbenen, Reisen, Familienfeiern, aber auch Examen und wichtige Dokumente können bei einem Ausfall verloren gehen. Dieser Verlust kann durch defekte Hardware, versehentliches Löschen, aber auch durch z.B. ein Verschlüsselungstrojaner, im Fachjargon Ransomware, passieren. Somit ist ein vernünftiges, geplantes Backup unabdingbar. Für jedes mögliche Szenario sollte also ein Backup zur Verfügung stehen. Ich habe meine jetzige Strategie einmal skizziert.

Meine Backup Strategie

Auf meinem Hauptrechner habe ich neben der C:\ Partition, auf der Windows installiert ist, eine D:\ Partition. Dort liegen meine Games, Bilder und virtuelle Maschinen. Auf das Backup von den Games und virtuellen Maschinen kann ich verzichten, da ich diese schnell aus dem Internet wieder bekommen kann. Wichtig waren für mich hier die Bilder. Diese sichere ich mit einem Skript auf meinem NAS. Das Skript führe ich manuell aus, sobald ich neue Fotos bzw. Fotos mit Lightroom bearbeitet habe. Manuell deswegen, weil mein Hauptrechner nach 30 Minuten Inaktivität automatisch in den Energiesparmodus geht. Damit kann ich keine reguläre „Online“ Zeit planen, wo das Skript automatisch ausgeführt wird. Alles was im Skript drin steht ist ein robocopy Befehl.

@echo off
robocopy D:\Picture\ X:\Picture\ /E /XO /FP /LOG:C:\Users\username\Desktop\pic.log
exit
/EKopiert Unterverzeichnisse, einschließlich leerer
Unterverzeichnisse.
/XOSchließt ältere Dateien aus.
/FPSchließt den vollständigen Pfadnamen von Dateien in
die Ausgabe ein.
/LOG:Gibt den Status der Protokolldatei aus (überschreibt das
vorhandene Protokoll).
Schalterbeschreibung

Das NAS

Somit wären die wichtigen Daten von meinem Rechner gesichert. Kommen wir nun also zum NAS. Auch hier läuft ein RAID 1. Das NAS ist ein QNAP TS-212, also schon etwas älter, verrichtet seine Aufgabe aber ohne Probleme. Auf dem NAS liegen nicht nur die Bilder vom Hauptrechner. Dort habe ich Filme, Serien, Musik und wichtige Dokumente abgelegt, die ich nicht direkt auf dem Hauptrechner haben wollte. Durch das Online Backup auf dem NAS habe ich mich gegen versehentliches Löschen und einen Hardwaredefekt abgedeckt. Der Schutz gegen Ransomware sollte zwingend offline passieren, denn wenn mein komplettes Netzwerk kompromittiert ist, sind im schlimmsten Fall nicht nur meine Daten auf dem Rechner verschlüsselt, sondern auch die auf dem NAS. Um dies zu verhindern, habe ich 2 externe Festplatten. Eine für gerade Wochen, die andere für ungerade. Eine Platte liegt also immer offline neben meinem NAS. Im Fall der Fälle hätte ich somit ein Vollbackup der vorherigen Woche zur Hand, was durchaus zu verscherzen ist.

Backup in die Cloud

Selbstverständlich habe ich mir auch Gedanken über ein Backup in die Cloud gemacht. Allerdings habe ich mich da gegen entschieden. Bei der Cloud bin ich auch auf den Anbieter angewiesen und muss ihn vollstes Vertrauen entgegen bringen können, das die Daten komplett sicher sind und nicht von Dritten gelesen werden können. Bei Fotos, Musik und anderen unwichtigen Dokumenten wäre mir dies egal und ein Backup in die Cloud wäre definitiv vorstellbar. Aber ich sichere ebenfalls wichtige Dokumente, wie Verträge, Rechnungen, etc. und die wollte ich nicht in der Cloud haben. Nichtsdestotrotz werde ich mir auch in Zukunft weiterhin Gedanken machen auch einige Dateien in die Cloud zu lagern, da ich bisher nur ein zentrales Backup habe. Gegen ein Feuer wäre ich also komplett ungeschützt und die Daten wären weg. Hier könnte ich mir aber auch ein zweites QNAP NAS bei meinen Eltern zu Hause vorstellen, die sich selbstständig synchronisieren. Dies ist bisher aber nur ein Gedankengang.

Falls ihr noch Anregungen habt, wie ich meine Strategie verbessern kann, schreibt mir die gerne in die Kommentare. Ihr könnt mir auch gerne schreiben, wie ihr selber euer Backup aufgebaut habt :).

Lüftersteuerung Raspberry Pi

Vor einiger Zeit hatte ich einen Beitrag über meinen Retropie geschrieben, welchen ich in einer Kiste verbaut hatte. Besagter Pi ist von der 4 Version. Hier soll die Wärmeentwicklung höher liegen als bei vorherigen Versionen. Die Kiste und die Spiele, die per Emulator gestartet werden, sorgen für eine höhere Wärmeentwicklung. Zuerst hatte ich einfache Kühlkörper auf den Chips kleben, die ich nun mit einem größeren Körper mit Lüfter ausgetauscht habe. Das Lüfter Kit* habe ich bei Amazon bestellt und kostet knappe 10€. Für die Spannungsversorgung wird ein 5V bzw 3.3V GPIO verwendet. Dabei läuft der Lüfter dauerhaft und trägt somit etwas zur Geräuschkulisse bei. Bei 5V selbstverständlich mehr als bei 3.3V, aber die Kühlleistung ist bei 5V dementsprechend auch höher. Allerdings wollte ich nicht, dass der Lüfter sich dauerhaft dreht und habe eine kleine Lüftersteuerung eingebaut. Für die Steuerung verwende ich einen Transistor, der den Stromkreis zum Lüfter schließt, sobald der Transistor über einen steuerbaren GPIO ein Signal bekommt.

Die Schaltung ist somit vollständig und wir können uns ans Programmieren wagen. Ich habe Python verwendet, es können allerdings auch andere Sprachen genommen werden. Den Quellcode habe ich in den Kommentaren erklärt. Da ich nicht wollte, dass der Lüfter in kurzen Intervallen seinen Status ändert (aus, an), habe ich eine Mindestlaufzeit des Lüfters von 10 Minuten integriert.

try:
    #versuche benötigte Module zu laden
    import gpiozero
    from time import sleep
    from datetime import datetime
    from datetime import timedelta

except:
    # springe raus, wenn erforderliche Module nicht vorhanden sind
    print("failed to load module")
    exit()

# Schwellenwert
THRESHOLD   = 35 #70
# Schlafzeit
SLEEP       = 10
# benutzter GPIO
GPIO        = 26 
# Helfervariable
STARTED     = None

#Funktion zur Temperaturüberprüfung.
# gibt die Temperatur zurück
def check_temperature():
    cpu = gpiozero.CPUTemperature()
    # gib die aktuelle Temperatur zurück
    return cpu.temperature

# main Funktion
if __name__ == "__main__":
    # initialisiere den GPIO
    led = gpiozero.LED(GPIO)
    
    while True:
        # hol die Temperatur und schreibe diese in temp
        temp = check_temperature()
        # gebe temp aus
        print(temp)

        # wenn temp größer gleich der Schwellenwert ist und STARTED den Status None hat
        if int(temp) >= THRESHOLD and not STARTED:
            # Setze den aktuellen Zeitstempel als STARTED
            STARTED = datetime.now()
            # schalte den GPIO an
            led.on()
            print("Fan not active and above Threshold, switch on")
        
        # wenn STARTED ist nicht None
        if STARTED:
            ''' Wenn der Zeit Stempel in STARTED + 10Min
                kleiner als dem jetzigen Zeit Stempel ist
                und die Temperatur kleiner gleich dem Schwellenwert ist
            '''
            if STARTED + timedelta(minutes=10) <= datetime.now() and int(temp) <= THRESHOLD:
                # setze STARTED = None
                STARTED = None
                # schalte den GPIO aus
                led.off()
                print("Fan active and underneath Threshold, switch off")
        
        # warte angegebene Zeit für die nächste Ausführung
        sleep(SLEEP)

Falls nötig, installiert folgende Module mit:

sudo apt update
sudo apt python3-pip
sudo pip3 install RPi.GPIO
sudo pip3 install gpiozero

Lüftersteuerung als Service

Bisher muss der Code selbstständig ausgeführt werden, damit die Lüftersteuerung aktiviert ist. Da ich mir das ganze ersparen wollte, habe ich die Lüftersteuerung in systemd als neuen Service integriert. Dafür legt eine neue Datei in /etc/system.d/system/ an. Ich habe meine fancontrol.service genannt.

[Unit]
Description=fancontrol Python
After=network.target
[Service]
ExecStart=/usr/bin/python3 -u fancontrol.py
WorkingDirectory=/home/pi/scripts
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi

[Install]
WantedBy=multi-user.target

Der Print-Output vom Script wird dabei von systemd in die Syslog geschrieben. Nun könnt ihr den Service mit systemctl stoppen, starten und überprüfen.

Eigenhändig könnt ihr den Service nun wie folgt starten:

sudo systemctl start fancontrol.service

Das Stoppen sieht ähnlich aus:

sudo systemctl stop fancontrol.service

Beim Überprüfen des Status gibt es noch eine Ausgabe über den aktuellen Zustand und in diesem Fall auch die Python Print Ausgabe.

sudo systemctl status fancontrol.service
● fancontrol.service - fancontrol Python
   Loaded: loaded (/etc/systemd/system/fancontrol.service; disabled; vendor preset: enabled)
   Active: active (running) since Sat 2021-01-02 22:18:38 GMT; 1min 35s ago
 Main PID: 2568 (python3)
    Tasks: 1 (limit: 4915)
   CGroup: /system.slice/fancontrol.service
           └─2568 /usr/bin/python3 -u fancontrol.py

Jan 02 22:18:49 retropie python3[2568]: 34.563
Jan 02 22:18:59 retropie python3[2568]: 34.076
Jan 02 22:19:09 retropie python3[2568]: 36.511
Jan 02 22:19:09 retropie python3[2568]: Fan not active and above Threshold, switch on
Jan 02 22:19:19 retropie python3[2568]: 36.511
Jan 02 22:19:29 retropie python3[2568]: 34.563
Jan 02 22:19:39 retropie python3[2568]: 35.05
Jan 02 22:19:49 retropie python3[2568]: 35.537
Jan 02 22:19:59 retropie python3[2568]: 34.563

OpenCV auf dem Raspberry Pi installieren

OpenCV ist ein Computer Vision Bibliothek und läuft unter Windows, Linux als auch macOS. Es wurde in C++ geschrieben, dementsprechend kann es dort inkludiert werden. Aber auch Python und Java werden von der Bibliothek unterstützt. Viele benutzen OpenCV zur Gesicht-, Gesten- und Objekterkennung, um nur einige zu nennen. In diesem Beitrag gehe ich auf die Installation ein. Auch wenn ich dies auf dem Raspberry Pi 3 mache, so ist die Routine auf anderen Debian-Derivaten ähnlich. Auf dem Pi3 läuft zurzeit Raspbian Stretch. Welche Version ihr habt, könnt ihr mit folgenden Befehl überprüfen:

pi@raspberrypi3:~ $ grep VERSION_CODENAME /etc/os-release
VERSION_CODENAME=stretch

OpenCV Installation

Zu unterscheiden sind hier 2 Installationsweisen. Die pip3-install Variante ist die schnellste, ist aber abhängig von den Maintainern des Repositories, wo das Packet geladen wurde. Mit anderen Worten, es könnte nicht aktuell sein oder einige benötigte Packages fehlen. Trotzdem sollte diese Variante nicht abgeschrieben werden. Sie reicht für die meisten privaten Projekte und das kompilieren von OpenCV hat viele Fallstricke.

pip3 installation

Wie schon erwähnt, ist diese Installation relativ einfach. Gebt dazu folgende Zeile in eurem Terminal ein:

sudo pip3 install opencv-contrib-python

Die, zu diesem Zeitpunkt aktuellste Version im Repository, ist die 4.4.0.46. Laut der offiziellen OpenCV Seite ist 4.5.0 die Aktuellste und müsste dementsprechend auf dem PI kompiliert werden. Falls ihr ein pip3 search opencv ausgeführt habt, so ist ebenfalls ein Package opencv-python zu finden. Der Unterschied ist, dass bei der contrib Versionen mehr Funktionen enthalten sind, daher ziehe ich die contrib Version der Standard vor.

OpenCV eigenständig kompilieren

Für die eigene Kompilierung müssen wir einige Packages selber holen und opencv von github beziehen.

Fangen wir an und laden uns die benötigten Entwicklungstools wie Cmake:

sudo apt update
sudo apt upgrade
sudo apt-get install build-essential cmake pkg-config

Zusätzlich benötigen wir I/O Packages um Video- und Bilddateien auf dem System zu lesen, bzw. vom Video Stream.

sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng-dev
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install libxvidcore-dev libx264-dev

Die folgenden Packages sind hilfreich und sollten ebenfalls mit installiert werden.

sudo apt-get install libfontconfig1-dev libcairo2-dev
sudo apt-get install libgdk-pixbuf2.0-dev libpango1.0-dev
sudo apt-get install libgtk2.0-dev libgtk-3-dev
sudo apt-get install libatlas-base-dev gfortran

Numpy sollte, falls nicht schon längst geschehen, installiert werden.

sudo pip3 install numpy

Damit hätten wir die erforderlichen Packages installiert. Wir können uns nun die aktuellste Version von openCV, Stand heute(05.12.2020) ist es die Version 4.5.0, aus dem Github Repository holen und entpacken.

wget -O opencv.zip https://github.com/opencv/opencv/archive/4.5.0.zip
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.5.0.zip
unzip opencv.zip
unzip opencv_contrib.zip

Anders als bei der pip3 Installation benötigen wir für die Contrib Installation auch die Standard Version. OpenCV installieren wir mit der Standard, geben beim Build die Contrib Version allerdings mit an.

Bevor wir nun weitermachen, ist es sinnvoll den SWAP zu erhöhen. Standardmäßig ist dieser auf 100MB begrenzt. Wir sollten vor der Kompilierung dem Raspberry erlauben 2048 MB vom Arbeitsspeicher auf der SD Karte auszulagern. Dafür muss in /etc/dphys-swapfile der FLAG CONF_SWAPSIZE auf 2048 geändert werden.

sudo vi /etc/dphys-swapfile
CONF_SWAPSIZE=2048
sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start

Wenn dieser gesetzt ist, können wir mit der Kompilierung weiter machen. Wechselt nun in das entpackte opencv Verzeichnis und legt ein neuen Ordner an. Diesen benötigen wir beim „Build“ Prozess, daher nennen wir ihn auch Build.

cd opencv-4.5.0/
sudo mkdir build
cd build

Nun können wir das Build konfigurieren. Achtet darauf, dass ihr auch wirklich im Build Ordner seid.

sudo cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-4.5.0/modules \
-D ENABLE_NEON=ON  \
-D ENABLE_VFPV3=ON \
-D BUILD_TESTS=OFF \
-D INSTALL_PYTHON_EXAMPLES=ON \
-D OPENCV_ENABLE_NONFREE=OFF \
-D CMAKE_SHARED_LINKER_FLAGS=-latomic \
-D BUILD_EXAMPLES=OFF ..

In OPENCV_EXTRA_MODULES_PATH gebt ihr den Pfad zu dem contrib Ordner an. Mit NEON und VFPv3 stellt ihr sicher, dass das Kompilieren schneller läuft und dass OpenCV für ARM Prozessoren optimiert wird. Falls ihr noch Beispiele für Python haben wollt, könnt ihr den FLAG INSTALL_PYTHON_EXAMPLES auf ON setzen. Die Konfigurierung dauert nun etwas. Danach wird eine Übersicht angezeigt, mit den Einstellungen die getroffen wurden.

-- General configuration for OpenCV 4.5.0 =====================================
--   Version control:               unknown
--
--   Extra modules:
--     Location (extra):            /home/pi/opencv_contrib-4.5.0/modules
--     Version control (extra):     unknown
--
--   Platform:
--     Timestamp:                   2020-12-03T19:48:35Z
--     Host:                        Linux 4.19.66-v7+ armv7l
--     CMake:                       3.7.2
--     CMake generator:             Unix Makefiles
--     CMake build tool:            /usr/bin/make
--     Configuration:               RELEASE
--
--   CPU/HW features:
--     Baseline:                    VFPV3 NEON
--       requested:                 DETECT
--       required:                  VFPV3 NEON
--
--   C/C++:
--     Built as dynamic libs?:      YES
--     C++ standard:                11
--     C++ Compiler:                /usr/bin/c++  (ver 6.3.0)
--     C++ flags (Release):         -fsigned-char -W -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wundef -Winit-self -Wpointer-arith -Wshadow -Wsign-promo -Wuninitialized -Winit-self -Wno-psabi -Wsuggest-override -Wno-delete-non-virtual-dtor -Wno-comment -fdiagnostics-show-option -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -mfpu=neon -fvisibility=hidden -fvisibility-inlines-hidden -O3 -DNDEBUG  -DNDEBUG
--     C++ flags (Debug):           -fsigned-char -W -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wundef -Winit-self -Wpointer-arith -Wshadow -Wsign-promo -Wuninitialized -Winit-self -Wno-psabi -Wsuggest-override -Wno-delete-non-virtual-dtor -Wno-comment -fdiagnostics-show-option -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -mfpu=neon -fvisibility=hidden -fvisibility-inlines-hidden -g  -O0 -DDEBUG -D_DEBUG
--     C Compiler:                  /usr/bin/cc
--     C flags (Release):           -fsigned-char -W -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wundef -Winit-self -Wpointer-arith -Wshadow -Wuninitialized -Winit-self -Wno-psabi -Wno-comment -fdiagnostics-show-option -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -mfpu=neon -fvisibility=hidden -O3 -DNDEBUG  -DNDEBUG
--     C flags (Debug):             -fsigned-char -W -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wundef -Winit-self -Wpointer-arith -Wshadow -Wuninitialized -Winit-self -Wno-psabi -Wno-comment -fdiagnostics-show-option -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -mfpu=neon -fvisibility=hidden -g  -O0 -DDEBUG -D_DEBUG
--     Linker flags (Release):      -latomic  -Wl,--gc-sections -Wl,--as-needed
--     Linker flags (Debug):        -latomic  -Wl,--gc-sections -Wl,--as-needed
--     ccache:                      NO
--     Precompiled headers:         NO
--     Extra dependencies:          dl m pthread rt
--     3rdparty dependencies:
--
--   OpenCV modules:
--     To be built:                 aruco bgsegm bioinspired calib3d ccalib core datasets dnn dnn_objdetect dnn_superres dpm face features2d flann freetype fuzzy gapi hfs highgui img_hash imgcodecs imgproc intensity_transform line_descriptor mcc ml objdetect optflow phase_unwrapping photo plot python2 python3 quality rapid reg rgbd saliency shape stereo stitching structured_light superres surface_matching text tracking ts video videoio videostab xfeatures2d ximgproc xobjdetect xphoto
--     Disabled:                    world
--     Disabled by dependency:      -
--     Unavailable:                 alphamat cnn_3dobj cudaarithm cudabgsegm cudacodec cudafeatures2d cudafilters cudaimgproc cudalegacy cudaobjdetect cudaoptflow cudastereo cudawarping cudev cvv hdf java js julia matlab ovis sfm viz
--     Applications:                perf_tests apps
--     Documentation:               NO
--     Non-free algorithms:         NO
--
--   GUI:
--     GTK+:                        YES (ver 3.22.11)
--       GThread :                  YES (ver 2.50.3)
--       GtkGlExt:                  NO
--     VTK support:                 NO
--
--   Media I/O:
--     ZLib:                        /usr/lib/arm-linux-gnueabihf/libz.so (ver 1.2.8)
--     JPEG:                        /usr/lib/arm-linux-gnueabihf/libjpeg.so (ver 62)
--     WEBP:                        /usr/lib/arm-linux-gnueabihf/libwebp.so (ver encoder: 0x0209)
--     PNG:                         /usr/lib/arm-linux-gnueabihf/libpng.so (ver 1.6.28)
--     TIFF:                        /usr/lib/arm-linux-gnueabihf/libtiff.so (ver 42 / 4.0.8)
--     JPEG 2000:                   build (ver 2.3.1)
--     OpenEXR:                     build (ver 2.3.0)
--     HDR:                         YES
--     SUNRASTER:                   YES
--     PXM:                         YES
--     PFM:                         YES
--
--   Video I/O:
--     DC1394:                      NO
--     FFMPEG:                      YES
--       avcodec:                   YES (57.64.101)
--       avformat:                  YES (57.56.101)
--       avutil:                    YES (55.34.101)
--       swscale:                   YES (4.2.100)
--       avresample:                NO
--     GStreamer:                   NO
--     v4l/v4l2:                    YES (linux/videodev2.h)
--
--   Parallel framework:            pthreads
--
--   Trace:                         YES (with Intel ITT)
--
--   Other third-party libraries:
--     Lapack:                      NO
--     Eigen:                       NO
--     Custom HAL:                  YES (carotene (ver 0.0.1))
--     Protobuf:                    build (3.5.1)
--
--   OpenCL:                        YES (no extra features)
--     Include path:                /home/pi/opencv-4.5.0/3rdparty/include/opencl/1.2
--     Link libraries:              Dynamic load
--
--   Python 2:
--     Interpreter:                 /usr/bin/python2.7 (ver 2.7.13)
--     Libraries:                   /usr/lib/arm-linux-gnueabihf/libpython2.7.so (ver 2.7.13)
--     numpy:                       /usr/local/lib/python2.7/dist-packages/numpy/core/include (ver 1.16.4)
--     install path:                lib/python2.7/dist-packages/cv2/python-2.7
--
--   Python 3:
--     Interpreter:                 /usr/bin/python3 (ver 3.5.3)
--     Libraries:                   /usr/lib/arm-linux-gnueabihf/libpython3.5m.so (ver 3.5.3)
--     numpy:                       /usr/local/lib/python3.5/dist-packages/numpy/core/include (ver 1.16.4)
--     install path:                lib/python3.5/dist-packages/cv2/python-3.5
--
--   Python (for build):            /usr/bin/python2.7
--
--   Java:
--     ant:                         /usr/bin/ant (ver 1.9.9)
--     JNI:                         NO
--     Java wrappers:               NO
--     Java tests:                  NO
--
--   Install to:                    /usr/local
-- -----------------------------------------------------------------
--
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pi/opencv-4.5.0/build

Falls Fehler aufgetaucht sind, müsst ihr diese natürlich vorher beheben. Da der Konfigurierungs-Prozess erfolgreich verlief, starten wir die Kompilierung. Ihr könnt es gerne ausprobieren, alle 4 Cores dabei zu benutzen.

sudo make -j4

In meinem Fall hat dies leider immer dazu geführt, dass der Raspberry dauerhaft abgestürzt ist. Erst als ich die Cores auf 1 reduziert hatte. Lief der Prozess komplett durch. Bei der Kompilierung müsst ihr Geduld mitbringen, denn die kann Gut und Gerne mal 4 Stunden dauern.

sudo make -j1

Ist die Kompilierung soweit erfolgreich abgeschlossen, geht es ans Installieren von OpenVC.

sudo make install

Nach der Installation dürft ihr nicht vergessen den SWAP wieder zu reduzieren, da es sonst zu Schäden auf der SD Karte kommen kann.

sudo vi /etc/dphys-swapfile
CONF_SWAPSIZE=100
sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start

Überprüft, ob OpenCV korrekt installiert ist und ermittelt die Versionsnummer.

pi@raspberrypi3:~/opencv-4.5.0/build $ opencv_version
4.5.0

Mit den zuvor installierten Python Beispielen könnt ihr einige Tests machen. Die Beispiele liegen im Ordner /usr/local/share/opencv4/samples/

Luftfeuchtigkeit mit raspberry messen

Der Raspberry bietet unzählige Möglichkeiten, diesen mit unterschiedlichen Sensoren zu erweitern. Mitunter einen Sensor zum Messen der Temperatur und der Luftfeuchtigkeit. Dies habe ich mir zunutze gemacht, um die Luftfeuchtigkeit in einigen Räumen zu messen. Zum Einsatz kommt der Sensor DHT11 mit 3 Pins, VCC(+), Ground(-) und Signal. Die 3 Pin Variante hat meist schon einen sogenannten 10k Ohm pull-up-Wiederstand verbaut. Im Elegoo-Set ist der genannte Sensor enthalten.

Zusammen mit dem Steckbrett habe ich auf dem Raspberry den 3.3V, den Ground und den GPIO No. 17 zur Datenübertragung verwendet. Die Steckzeichnung findet ihr im unterem Bild.

Somit wäre der Sensor mit dem PI verbunden und wir können uns ans Auslesen der Daten wenden. Dafür verwende ich Python3 mit dem Modul „Adafruit_Python_DHT“. Dies könnt ihr mittels pip3 herunterladen

sudo pip3 install Adafruit_DHT

Habt ihr pip3 noch nicht installiert, könnt ihr das wie folgt nachholen.

sudo apt install python3-pip

Der Code ist zurzeit relativ einfach gehalten. In einer while Schleife wird die Temparatur/Luftfeuchtigkeit einmal pro Minute ausgelesen und auf dem Terminal ausgegeben.

#load required modules
import Adafruit_DHT
import datetime as dt
import time

#define used sensor
sensor = Adafruit_DHT.DHT11

#define used pin on RPI
pin = 17

while True:
    #get humidity/temperature
    humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

    if humidity is not None and temperature is not None:
        print('[{0}]  Temp={1:0.1f}*C  Humidity={2:0.1f}%'.format(dt.datetime.now(),temperature, humidity))
    else:
        print('Pin kann nicht gelesen werden, erneut versuchen!')
    time.sleep(60)

Die Ausgabe sieht dann wie folgt aus, bis das Script manuell beendet wird:


[2020-12-02 20:22:52.091902]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:23:52.632069]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:24:53.221121]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:25:53.810168]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:26:54.341988]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:27:57.461005]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:28:58.050115]  Temp=21.0*C  Humidity=54.0%
[2020-12-02 20:29:58.592045]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:30:59.181064]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:31:59.770323]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:33:00.342131]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:34:00.931251]  Temp=21.0*C  Humidity=53.0%
[2020-12-02 20:35:01.512562]  Temp=21.0*C  Humidity=53.0%