Note that these displays are not synced. That would be a whole ordeal. In fact we thought it would be cool to display different pictures. So the second monitor will be showing yesterday’s slideshow from the main monitor.
Automating turn-on, turn-off of the HDMI display based on the ambient room light
Since this second slideshow is in a bedroom, I wanted to have it turn off when the lights were out, and turn back on again during daylight. This was a really interesting challenge for me as I got to use an inexpensive external sensor with my RPi. And I got it to work, and it works quite well if I say so myself. That’s all written up in this post.
#!/bin/sh
# DrJ 1/2021
# call this from cron once a day to refesh random slideshow once a day
NUMFOLDERS=20
DEBUG=1
HOME=/home/pi
RANFILE=$HOME/random.list
REANFILE=$HOME/rean.list
DISPLAYFOLDER=$HOME/Pictures
DISPLAYFOLDERTMP=$HOME/Picturestmp
EXIFTMP=$HOME/EXIFtmp
EXIF=$HOME/EXIF
TXTDIR=$HOME/picstxt
MSHOW=$HOME/mediashow
MSHOW2=$HOME/mediashowtmp2
MSHOW3=$HOME/mediashowtmp3
SLEEPINTERVAL=1
STARTFOLDER="MaryDocs/Pictures and videos"
echo "Starting master process at "`date`
cd $HOME
rm -rf $DISPLAYFOLDERTMP
mkdir $DISPLAYFOLDERTMP
#listing of all Google drive files starting from the picture root
# this takes a few minutes so we may want to skip for debugging
if [ "$1" = "skip" ]; then
if [ $DEBUG -eq 1 ]; then echo SKIP Listing all files from Google drive; fi
else
if [ $DEBUG -eq 1 ]; then echo Listing all files from Google drive; fi
rclone ls remote:"$STARTFOLDER" > files
# filter down to only jpegs, lose the docs folders and the tiny JPEGs
if [ $DEBUG -eq 1 ]; then echo Picking out the JPEGs and losing the small images; fi
egrep '\.[jJ][pP][eE]?[gG]$' files |awk '$1 > 11000 {$1=""; print substr($0,2)}'|grep -i -v /docs/ > jpegs.list
fi
# check if we got anything. If our Internt dropped there may have been a problem, for instance
flines=`cat files|wc -l`
if [ $flines -lt 60 ]; then
echo "rclone did not produce enough files. Check your Internet setup and rclone configuration."
echo Only $flines files in the file listing - not enough - so pausing 60 seconds and starting over... at `date`
# start a new job and kill ourselves!
nohup $HOME/master3.sh > master.log 2>&1 &
exit
fi
# throw NUMFOLDERS or so random numbers for picture selection, select triplets of photos by putting
# names into a file
if [ $DEBUG -eq 1 ]; then echo "\nGenerate random filename triplets"; fi
./random-files3.pl -f $NUMFOLDERS -j jpegs.list -r $RANFILE
# copy over these 60 jpegs
if [ $DEBUG -eq 1 ]; then echo "\nCopy over these random files"; fi
cat $RANFILE|while read line; do
if [ $DEBUG -eq 1 ]; then echo filepath is $line; fi
rclone copy remote:"${STARTFOLDER}/$line" $DISPLAYFOLDERTMP
sleep $SLEEPINTERVAL
done
# do a re-analysis to push pictures further apart in time
if [ $DEBUG -eq 1 ]; then echo "\nRe-analyzing pictures for their timestamps"; fi
cd $DISPLAYFOLDERTMP; $HOME/reanalyze.pl
# copy over just the new pictures that we determined were needed
if [ $DEBUG -eq 1 ]; then echo "\nCopy over the needed replacement files"; fi
cat $REANFILE|while read line; do
if [ $DEBUG -eq 1 ]; then echo filepath is $line; fi
rclone copy remote:"${STARTFOLDER}/$line" $DISPLAYFOLDERTMP
sleep $SLEEPINTERVAL
done
# QC: toss out the pics which are not actually JPEGs
if [ $DEBUG -eq 1 ]; then echo "\nQC: Toss out the pics which are not actually JPEGs"; fi
cd $DISPLAYFOLDERTMP; ../QC.pl
# save EXIF metadata for later
if [ $DEBUG -eq 1 ]; then echo "\nSave EXIF metadata for later"; fi
cd $DISPLAYFOLDERTMP; $HOME/get-all-EXIF.sh
rm -rf $EXIF;mv $EXIFTMP $EXIF
# analyze EXIF info to extract most interesting things
if [ $DEBUG -eq 1 ]; then echo "\nAnalyze EXIF data"; fi
rm -rf $TXTDIR; $HOME/analyze.sh
# rotate pics as needed
if [ $DEBUG -eq 1 ]; then echo "\nRotate the pics which need it"; fi
cd $DISPLAYFOLDERTMP; $HOME/rotate-as-needed.sh
# resize pics
if [ $DEBUG -eq 1 ]; then echo "\nSize all pics to the display size"; fi
$HOME/resize.sh
# create text info + images
if [ $DEBUG -eq 1 ]; then echo "\nEmbed pic info"; fi
$HOME/embedpicinfo.sh
cd ~
# kill any old slideshow
if [ $DEBUG -eq 1 ]; then echo Killing old fbi slideshow; fi
sudo pkill -9 -f fbi
pkill -9 -f m3.pl
# remove old pics
if [ $DEBUG -eq 1 ]; then echo Removing old pictures; fi
rm -rf $DISPLAYFOLDER
mv $DISPLAYFOLDERTMP $DISPLAYFOLDER
cp $MSHOW3 $MSHOW
touch refresh
#run looping fbi slideshow on these pictures
if [ $DEBUG -eq 1 ]; then echo Start "\nfbi slideshow in background"; fi
cd $DISPLAYFOLDER ; nohup ~/m3.pl >> ~/m3.log 2>&1 &
if [ $DEBUG -eq 1 ]; then echo "And now it is "`date`; fi
#!/usr/bin/perl
use Getopt::Std;
my %opt=();
#
# assumption is that we are runnin this from a directory containing pictures
$tier1 = 100; $tier2 = 200; $tier3 = 300; # secs
$DEBUG = 1;
$HOME = "/home/pi";
# pics are here
$pNames = "$HOME/reanpicnames";
$ranfile = "$HOME/random.list";
$reanfile = "$HOME/rean.list";
$origfile = "$HOME/jpegs.list";
$mshowt = "$HOME/mediashowtmp";
$mshow2 = "$HOME/mediashowtmp2";
open(REAN,">$reanfile") || die "Cannot open reanalyze file $reanfile!!\n";
$ms = `cat $mshowt`;
print "Original media show: $ms\n" if $DEBUG;
@lines = split('\0',$ms);
$Pdate = $Phr = $Pmin = $Psec = 0;
$diff = 9999;
for($i=0;$i<@lines;$i++){
$date = 0;
$secs = $ymd = 0;
$_ = $lines[$i];
$file = $_;
# ignore pictures with names like 20130820_180050.jpg
next if /\d{8}_\d{4}/;
open(ANAL,"$HOME/getinfo.py \"$file\"|") || die "Cannot open file: $file!!\n";
print "filename: $file\n" if $DEBUG;
while(<ANAL>){
#extract date and time from remaining pictures, if possible
# # DateTimeOriginal = 2018:08:18 20:16:47
# print STDERR "DATE: $_" if $DEBUG;
if (/date/i && $date++ < 1) {
print "date match in getinfo.pyoutput: $_" if $DEBUG;
($ymd,$hr,$min,$sec) = /(\d{4}:\d\d:\d\d) (\d\d):(\d\d):(\d\d)/;
$secs = 3600*$hr + 60*$min + $sec;
print "file,secs,ymd,i: $file,$secs,$ymd,$i\n" if $DEBUG;
$YMD[$i] = $ymd;
$SECS[$i] = $secs;
}
} # end loop over analysis of this pic
} # end loop over all files
# now go over that
$oldfolder = 0;
for($i=1;$i<@lines;$i++){
$folder = int($i/3) + 1;
next unless $folder != $oldfolder;
print "analyzing results. folder no. $folder\n" if $DEBUG;
# analyze pics in triplets
# center pic
$j = ($folder - 1)*3 + 1;
for ($o=-1;$o<2;$o+=2){
$k=$j+$o;
print "j,k,o: $j,$k,$o\n" if $DEBUG;
next unless $SECS[$j] > 0 && $YMD[$j] == $YMD[$k] && $YMD[$j] > 0;
print "We have non-0 dates we're dealing with\n" if $DEBUG;
$file = $lines[$k];
chomp($file);
$diff = abs($SECS[$j] - $SECS[$k]);
print "diff: $diff\n" if $DEBUG;
next unless $diff < $tier3;
# the closer the files are together the more we push away
$bump = 1 if $diff < $tier3;
$bump = 2 if $diff < $tier2;
$bump = 3 if $diff < $tier1;
# get full filepath
$filepath = `grep \"$file\" $ranfile`;
chomp($filepath);
# now use that to search within the jpegs file listing
$prog = $o < 0 ? "head" : "tail";
$newfilepath = `grep -C$bump "$filepath" $origfile|$prog -1`;
($newfile) = $newfilepath =~ /([^\/]+)$/;
chomp($newfile);
print "file,filepath,newfile,newfilepath,bump: $file,$filepath,$newfile,$newfilepath,$bump\n" if $DEBUG;
print REAN $newfilepath;
# we'll get the new pictures over in a separate step to keep this more atomic
$ms =~ s/$file/$newfile/;
}
$oldfolder = $folder;
} # end loop over pics
# print out new mediashow pics in order
print "Printing new mediashow: $ms\n" if $DEBUG;
open(MS,">$mshow2") || die "Cannot open mediashow $mshow2!!\n";
print MS $ms;
close(MS)
#!/usr/bin/perl
# kick out the non-JPEG files - sometimes they creep in
$DEBUG = 1;
$HOME = "/home/pi";
$mshow2 = "$HOME/mediashowtmp2";
$mshow3 = "$HOME/mediashowtmp3";
$ms = `cat $mshow2`;
@pics = split('\0',$ms);
foreach $file (@pics) {
print "file is $file\n" if $DEBUG;
#DSC00185.JPG: JPEG image data, JFIF standard 1.01...
$res = `file "$file"|cut -d: -f2`;
if ($res =~ /JPEG/i){
print "This file is indeed a JPEG image\n" if $DEBUG;
} else {
print "Not a JPEG image! We have to remove this file form the mediashow\n" if $DEBUG;
$ms =~ s/$file\0//;
}
}
# print out new mediashow pics in order
print "Printing new mediashow: $ms\n" if $DEBUG;
open(MS,">$mshow3") || die "Cannot open mediashow $mshow3!!\n";
print MS $ms;
close(MS);
#!/bin/sh
# DrJ 1/2021
# preserve EXIF info of all the images because our rotate step removes it
# and we will use it in subsequent steps
# assumption is that our current directory is the one where we want to read files
EXIFTMP=~/EXIFtmp
mkdir $EXIFTMP
ls -1|while read line; do
echo file is "$line"
~/getinfo.py "$line" > $EXIFTMP/"$line"
done
#!/bin/sh
# DrJ 1/2021
# try to extract date, file and folder name and even GPS info, create jpegs with info
# for each image
# assumption is that are current directory is the one where we want to alter files
HOME=/home/pi
TXTDIR=$HOME/picstxt
# it's assumed EXIF info for each pic has already been extracted and put into EXIF diretory
EXIF=$HOME/EXIF
mkdir $TXTDIR
cd $EXIF
ls -1|while read line; do
echo file is "$line"
echo -n "$line"|../analyzeDate.pl > "$TXTDIR/${line}"
echo -n "$line"|../analyzeGPS.pl >> "$TXTDIR/${line}"
done
#!/bin/sh
# DrJ 12/2020
# some of our downloaded files will be sideways, and fbi doesn't auto-rotate them as far as I know
# assumption is that our current directory is the one where we want to alter files
ls -1|while read line; do
echo file is "$line"
o=`~/getinfo.py "$line"|grep -ai orientation|awk '{print $NF}'`
echo orientation is $o
if [ "$o" -eq "6" ]; then
echo "90 clockwise is needed, o is $o"
# rotate and move it
~/rotate.py -90 "$line"
mv rot_"$line" "$line"
elif [ "$o" -eq "8" ]; then
echo "90 counterclock is needed, o is $o"
# rotate and move it
~/rotate.py 90 "$line"
mv rot_"$line" "$line"
elif [ "$o" -eq "3" ]; then
echo "180 rot is needed, o is $o"
# rotate and move it
~/rotate.py 180 "$line"
mv rot_"$line" "$line"
fi
done
#!/bin/sh
# DrJ 2/2021
# To combat the RPi's inherent sluggish performance we'll downsize the pictures in advance to save fbi the effort
#
# on the pidisplay fbset gives:
#mode "800x480"
# geometry 800 480 800 480 32
# timings 0 0 0 0 0 0 0
# rgba 8/16,8/8,8/0,8/24
#endmode
displaywidth=`fbset|grep geometry|awk '{print $2}'`
displayheight=`fbset|grep geometry|awk '{print $3}'`
ls -1|while read line; do
echo file is "$line"
# this will create a new image with same name prepended with txt_
~/embedpicinfo.py $displaywidth $displayheight "$line"
done
#!/usr/bin/python3
# call with two arguments: degrees-to-rotate and filename
import PIL, os
import sys
from PIL import Image
# first do: pip3 install piexif
import piexif
degrees = int(sys.argv[1])
pic = sys.argv[2]
picture= Image.open(pic)
# see https://github.com/hMatoba/Piexif for piexif writeup
# this method of preserving EXIF info does not always work, and
# causes script to crash when it fails!
##exif_dict = piexif.load(picture.info["exif"])
##exif_bytes = piexif.dump(exif_dict)
## both rotate and preserve EXIF data
##picture.rotate(degrees,expand=True).save("rot_" + pic,"jpeg", exif=exif_bytes)
# rotate (which will blow away EXIF info, sorry...)
picture.rotate(degrees,expand=True).save("rot_" + pic,"jpeg")
#!/usr/bin/python3
# DrJ 2/2021
import PIL, os
import sys
from PIL import Image
# somewhat inspired by http://www.riisen.dk/dop/pil.html
# arguments:
# <width> <height> file
# with and height should be provided as values in pixels
# image file should be provided as argument.
# A pidisplay is 800x480
displaywidth = int(sys.argv[1])
displayheight = int(sys.argv[2])
smallscreen = 801
imageFile = sys.argv[3]
im1 = Image.open(imageFile)
narrowmax = .76
blowupfactor = 1.1
# take less from the top than the bottom
topshare = .3
bottomshare = 1.0 - topshare
# for DrJ debugging
DEBUG = True
if DEBUG:
print("display width and height: ",displaywidth,displayheight)
def imgResize(im):
width = im.size[0]
height = im.size[1]
if DEBUG:
print("image width and height: ",width,height)
# If the aspect ratio is wider than the display screen's aspect ratio,
# constrain the width to the display's full width
if width/float(height) > float(displaywidth)/float(displayheight):
if DEBUG:
print("In section width contrained to full width code section")
widthn = displaywidth
heightn = int(height*float(displaywidth)/width)
im5 = im.resize((widthn, heightn), Image.ANTIALIAS) # best down-sizing filter
else:
heightn = displayheight
widthn = int(width*float(displayheight)/height)
if width/float(height) < narrowmax and displaywidth < smallscreen:
# if width is narrow we're losing too much by using the whole picture.
# Blow it up by blowupfactor% if display is small, and crop most of it from the bottom
heightn = int(displayheight*blowupfactor)
widthn = int(width*float(heightn)/height)
im4 = im.resize((widthn, heightn), Image.ANTIALIAS) # best down-sizing filter
top = int(displayheight*(blowupfactor - 1)*topshare)
bottom = int(heightn - displayheight*(blowupfactor - 1)*bottomshare)
if DEBUG:
print("heightn,top,widthn,bottom: ",heightn,top,widthn,bottom)
im5 = im4.crop((0,top,widthn,bottom))
else:
im5 = im.resize((widthn, heightn), Image.ANTIALIAS) # best down-sizing filter
im5.save("resize_" + imageFile)
imgResize(im1)
#!/usr/bin/perl
# show the pics ; rotate the screen as needed
# for now, assume the display is in a neutral
# orientation at the start
use Time::HiRes qw(usleep);
$DEBUG = 1;
$delay = 6; # seconds between pics
###$delay = 4; # for testing
$mdelay = 200; # milliseconds
$mshow = "$ENV{HOME}/mediashow";
$pNames = "$ENV{HOME}/pNames";
# pics are here
$picsDir = "$ENV{HOME}/Pictures";
$refreshFile = "$ENV{HOME}/refresh";
chdir($picsDir);
$cn = `ls -1|wc -l`;
chomp($cn);
print "$cn files\n" if $DEBUG;
# throw up a first picture - all black. Trick to make black bckgrd permanent
system("sudo fbi -a --noverbose -T 1 $ENV{HOME}/black.jpg");
# see if this is a new batch of pictures
$refresh = (stat($refreshFile))[9];
$now = time();
$diff = $now - $refresh;
print "refresh,now,diff: $refresh, $now, $diff\n" if $DEBUG;
if ($diff < 100){
system("sudo fbi -a --noverbose -T 1 $ENV{HOME}/newslideshowintro.jpg");
sleep(25);
}
system("sudo fbi -a --noverbose -T 1 $ENV{HOME}/black.jpg");
system("sleep 1; sudo killall fbi");
# start infinitely looping fbi slideshow
for (;;) {
# then start slide show
# shell echo cannot work with null character so we need to use a file to store it
system("sudo xargs -a $mshow -0 fbi --noverbose -1 -T 1 -t $delay ");
###system("sudo xargs -a $mshow -0 fbi -a -1 -T 1 -t $delay "); # for testing
# fbi runs in background, then exits, so we need to monitor if it's still alive
for(;;) {
open(MON,"ps -ef|grep fbi|grep -v grep|") || die "Cannot launch ps -ef!!\n";
$match = <MON>;
if ($match) {
print "got fbi match\n" if $DEBUG > 1;
} else {
print "no fbi match\n" if $DEBUG;
# fbi not found
last;
}
close(MON);
print "usleeping, noexist is $noexit\n" if $DEBUG > 1;
usleep($mdelay);
} # end loop testing if fbi has exited
} # close of infinite loop
Optional script
mshowtmp.pl (revision not yet reflected in the tar file)
# display some ip info first
@reboot sleep 15;ip a|grep wlan|sudo tee -a /dev/console > /dev/null
@reboot sleep 22; ./m3.pl >> m3.log 2>&1
# reboot if we can’t reach the Internet
19 5 */2 * * curl google.com > /dev/null 2>&1 || sudo reboot
26 5 */2 * * ./master3.sh >> master.log 2>&1
That will refresh the slideshow every two days, which we found is a good interval for our lifestyle – some days you don’t get around to viewing them. If you want to refresh every day just change ‘*/2″ to ‘*’.
And… that’s it!
Reminder
Don’t forget to make all these files executable. Something like:
$ chmod +x *.pl *.py *.sh
should do it.
My equipment
RPi 3 running Raspbian Lite, OS version “Bullseye,” though older versions also work well, just accommodating the appropriate packages which have changed over time.
Pi Display. The Pi Display resolution is 800×480, so pretty small.
HDMI display such as a TV as alternate to a Pi Display. This does work! I just tested this in Jan, 2022. My Sony TV display resolution is 1920 x 1080.
Pre-install
There are a few things you’ll need (accurate statement as of OS Bullseye, Jan 2022) such as these system packages: fbi, file, rclone, and these python modules: pip, Pillow, and piexif. That’s mostly described in my previous post so I won’t repeat it here. Basically the system package you install with apt-get. After installing pip you use it to install Pillow and piexif.
Getting started
To see how badly things are going for you (hey, I like to be cautiously pessimistic) after you’ve created all these files and have installed rclone, do a
$ ./master3.sh
If you have your rclone file listing (which takes a long time) and want to focusing on debugging the rest of it, do a
$ ./master3.sh skip
Discussion
In this version of Raspberry Pi photo frame I’ve made more effort to force time separation between the randomly selected photos. But, that’s not all. I blow up pictures taken in a narrow (portrait) mode (see next paragraph). And I do some fancy analysis to determine filename, folder, date, time and even location of the pictures. And there’s more. I create an alternate version of each photo which embeds this info at the bottom – in anticipation of my even more fancy remote-controlled slideshow! I am afraid to overwrite what I have previously posted because that by itself is a complete solution and works quite well on its own. So this can be considered worthy of folks looking for a little more challenge to get better results.
The fancyresize.py script is designed around my small PiDisplay which has a horizontal resolution of only 800 pixels. It blows up a narrow, portrait-format picture only if the detected display has a horizontal resolution of no more than 800 pixels. It blows the picture up by 10%, chops off 3% from the top, 7% from the bottom, because that yields optimal results in my experience. If you like that approach but are using a larger HDMI display, you could edit the “801” in that file to make it a larger number (bigger than your display, like 5000).
Show pictures with embedded info
This process is not streamlined. But it can be cool to do it by hand. You could follow these steps.
$ ./mshowtmp.pl; mv mediashowtmp2 mediashow
If you wait the whole cycle the next time around it should display the pictures with the embedded info at the bottom. If you’re impatient, do this:
I’m seeing this while transferring the pictures. Guess I’ll have to slow down the transfer. Not sure. Still figuring this out.
Fun Fact
You know how those old digital cameras created files prefixed with DSC, like DSC00102.JPG? If you read the JPEG spec, which is a pretty dense document, you learn that DSC stands for Digital Still Camera.
Concept for tossing out pictures of documents
We sometimes take pictures of documents, or computer screens, or a slide at a presentation, or a historical marker. They don’t make for compelling slideshow material. Well, the historical markers are debatable since they have character. Anyway, I am looking at using an old open source program called tesseract to do OCR (optical character recognition) on all the photos to help identify those containing a lot of words so they can be excluded. I’ll include that if I determine it to be a good approach.
Installing a searchable dictionary on Raspberry Pi
To install a word dictionary that you can do simple searches against on an RPi, try:
$ sudo apt-get install wamerican
or maybe
$ sudo apt-get install wamerican-huge
Those will produce simple wordlists, not actual dictionaries with definitions as you might have expected. They go into /usr/share/dict, e.g., usr/share/dict/american-english-huge.
The dict program is quite nice. apt-get install dict. Then you run it like this
$ dict neume
and it shoots back definitions and cites sources for those definitions. The drawback for my purposes is that it uses your Internet connection and I’m trying to build a photo frame that doesn’t rely too much on the Internet after the photos themselves are fetched.
git clone https://github.com/thortex/rpi3-tesseract.git cd rpi3-tesseract cd release ./install_requires_related2leptonica.sh ./install_requires_related2tesseract.sh ./install_tesseract.sh
But you’re gonna need git first:
$ sudo apt-get install git
RPi lost Wifi
This could be a whole separate post. In the course of my hard work my RPi just would not acquire an IP address on wlan0.
Here’s a great command to see all the SSIDs it knows about:
$ sudo iwlist wlan0 scan > scan.log
Then you can inspect scan.log in an editor. Turns out the one SSID it needed wasn’t in the list. Turns out I had reserved a DHCP entry for it in my router. My router was simply not cooperating, it seems – the RPi wasn’t doing anything wrong. I was almost ready to re-install the whole thing and waste hours… My router is an older model Linksys WRT1200AC. I removed the DHCP reservation on the router, then did a
$ sudo service networking stop; sudo service networking start
on the RPi, and…all was good! Its assigned IP won’t change that often, I can always check the router to see what it is. The management software with the Linksys is quite good.
I’m still having IP issues. So I added an additional crontab entry to display IP info of the WiFi adapter for a few seconds before the slideshow kicks in. This will tell me if it at least has an IP (which it often doesn’t)! It’s a pretty clever idea if I say so myself – the way I managed to do it that is. It’s not in the tar file.
RPi partially blown up
I’ve been running the photo frame for about two years now. All the residents love to see when the pictures refresh what the new slideshow brings. But in all the work I’ve done here and there I’ve partially blown up the RPi. Symptoms: running curl produces a segmentation fault; running crontab -e produces crontab: “/usr/bin/sensible-editor” exited with status 2; and then there’s the fact I lose my IP after a few days. I bet there’s a lot else that’s wrong too, but the sldieshow stuff keeps chugging along, amazingly. I’m too unmotivated (lazy) to fix all these problems, except the IP thing. That prevents slideshow refreshes. So I’ve decided to script a reboot command to run before the slideshow refresh. The resulting conditional reboot is now incorporated into the crontab entries shown earlier.
And by the way, I did fix this problem by re-imaging the micro SD card. That brought a new problem which is that the display blanked out afew only a few seconds. I bought a new display (turns out I didn’t need to), still had the problem, then figured out how to fix it. I wrote up the fix in this post.
Conclusion
A more advanced treatment of photos is shown in this post than I have done previously. It is fairly robust and will withstand quite a few user errors in my experience. The end result will be an interesting display of your photos, randomly selected but in small groupings.
m3.pl refers to a black.jpg and a newslideshowintro.jpg file. It’s not a disaster to not have those, but the overall experience will be slightly better. Here’s black.jpg:
And the beautiful newslideshowintro.jpg I created is at the top of this blog post.
Tesseract, an surprisingly old and surprisingly good OCR open-source OCR program, is basically impossible to compile for RPi. Fortunately, someone has done it for us. This page has the instructions: https://github.com/thortex/rpi3-tesseract