Category Archives: Computer

Hugin / generic panorama injector for immich

I have a bunch of panorama’s, made with phones or my Niko DLSR stitched with Hugin.
These are not being detected as panorama by Immich, due to missing panorama tags …
Here is my fix

./makepanorama2.sh '750_5488_resize - 750_5532_resize.jpg' --flip
Input:       750_5488_resize - 750_5532_resize.jpg
Orientation: Horizontal (normal)
Applying EXIF orientation...
Flipping image vertically...
Image size:  8712 x 846
Full panorama: 8712 x 4356
Crop:          8712 x 846
Crop top:      1755
    1 image files updated

Created: ./injected-750_5488_resize - 750_5532_resize.jpg

GPano:
[File]          ImageWidth                      : 8712
[File]          ImageHeight                     : 846
[IFD0]          Orientation                     : Rotate 180
[XMP-GPano]     ProjectionType                  : equirectangular
[XMP-GPano]     FullPanoWidthPixels             : 8712
[XMP-GPano]     FullPanoHeightPixels            : 4356
[XMP-GPano]     CroppedAreaImageWidthPixels     : 8712
[XMP-GPano]     CroppedAreaImageHeightPixels    : 846
[XMP-GPano]     CroppedAreaLeftPixels           : 0
[XMP-GPano]     CroppedAreaTopPixels            : 1755

From:

To:

Immich media fixer

I’ve got a bunch of media in my library with the following issues:

  • Images : too dark (rotation can be done within Immich)
  • Videos : too dark, interlaced or rotated

I used to fix these by removing from Immich, change original media, and upload again.
Then I had to fix the date of media with missing exif dates, using touch command.

Now I have a script which is doing the following:

  • Copy the URL containing the media
  • It downloads the media using API calls
  • It downloads the metadata for dates and tags
  • I calls a second python script in which you can do the following:
    Change contrast/brightness, rotate CW or CCW, and select deinterlace when needed.
  • Write the new image or movie to a temp file
  • When exiting the editor the first called python script uploads the altered media and fixes tags and dates
  • After that some jobs are being started: Thumbnail recreation, Encoded videos, metadata and face recognition.

Takes only a few seconds to fix an asset

No code, because of some Vibe coding.
PM me if interested ..

Immich hints and tips part 2

FIX rotation of movie

usage rotatefix movie001.mpg 90 (or -90)

If your data is in a external library, you can do this on the file in the library. After that you have to select it and do the following:

  • Refresh encoded videos
  • Refresh metadata
  • Refresh thumbnails
#!/bin/bash
#set -x
if [ "$#" -lt 2 ]; then
  echo >&2 'fix filename 90||-90||0'
  exit 1
fi

fix=0
exiftool $1  | grep "^Create Date" | grep 0000 && fix=1 

dater=$(realpath $1| grep -oE '[0-9]{8}' | head -1 )
if [ $2 == "90" ] ; then
	ffmpeg -i $1  -vf 'transpose=1' -c:a copy  tmp_${1} 
mv tmp_${1} $1
fi
if [ $2 == "-90" ] ; then
	ffmpeg -i $1  -vf 'transpose=2' -c:a copy  tmp_${1} 
mv tmp_${1} $1
fi
if [ $fix == "1" ] ; then
	echo "Fixing date"
	touch -t ${dater}1200.00 $1
fi

Get location of file by pressing the (i)

Deinterlace video and touch date, when no exif date is embedded

#!/bin/bash

mkdir -p deinterlace

for video in DV*; do
    [ -f "$video" ] || continue

    : >2.log
    echo "testing $video"

    ffmpeg -i "$video" -filter:v idet -frames:v 500 -an -f rawvideo -y /dev/null 2>2.log

    inter=$(
        grep Parsed 2.log |
        tail -2 |
        cut -d: -f2- |
        awk '
        {
            inter += $2 + $4
            prog += $6
        }
        END {
            print (inter > prog ? 1 : 0)
        }'
    )

    if [ "$inter" -eq 1 ]; then
        echo "convert $video"
         ffmpeg -i "$video" -vf bwdif -c:v libx264 -crf 18 -preset medium \
             -c:a aac -b:a 192k "deinterlace/${video%.*}-deinterlaced.mp4"
    fi

    dt=$(echo "$video" | grep -oE '[0-9]{2}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}' | tr '_' ' ' | tr '-' ' ')

if [ -n "$dt" ]; then
    # convert DD-MM-YY HH-MM to YYYY-MM-DD HH:MM
    read year month day hour minute <<< "$dt"

    # assume 20xx for year
    year=$((2000 + year))

    touch -d "$year-$month-$day $hour:$minute:00" "deinterlace/${video%.*}-deinterlaced.mp4"
    touch -d "$year-$month-$day $hour:$minute:00" "$video"
fi
done

FIX DATES FILES in /2006/20060701/ structure

This touches files which have no exif info, to the date of the directory to that date with time 12:00

find . -type f -iname "*.mpg" | while read -r file; do     ymd=$(echo "$file" | grep -oE '[0-9]{8}' | head -1);     touch -t "${ymd}1200.00" "$file"; done

Touch date of current directory to date in path

for f in *; do
    [[ $f =~ ([0-9]{8}) ]] || continue
    touch -t "${BASH_REMATCH[1]}1200.00" "$f"
done

Broken thumb nail fix

# goto movie file in your library
# Add flags using below command
ffmpeg -i 73a1a221be4c.mp4 -c copy -video_track_timescale 90000 -movflags +faststart output.mp4
# replace movie file
cat output.mp4 > 73a1a221be4c.mp4 
rm output.mp4
# then fresh encoded videos in immich, then refresh metadata and thumbnails.

FIX Date unknown or incorrect dates

#!/bin/bash
if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <movie-file> <YYYYMMDD>"
    exit 1
fi

FILE="$1"
DATE="$2"

# Validate date format
if [[ ! "$DATE" =~ ^[0-9]{8}$ ]]; then
    echo "Error: date must be YYYYMMDD"
    exit 1
fi

# Convert YYYYMMDD -> YYYY:MM:DD 12:00:00 for EXIF metadata
YEAR="${DATE:0:4}"
MONTH="${DATE:4:2}"
DAY="${DATE:6:2}"
EXIFDATE="${YEAR}:${MONTH}:${DAY} 12:00:00"

# touch format: YYYYMMDD1200.00
TOUCHDATE="${DATE}1200.00"

echo "File:       $FILE"
echo "Date:       $DATE"
echo "Metadata:   $EXIFDATE"
echo "Filesystem: $TOUCHDATE"

# Change movie metadata dates
exiftool -overwrite_original \
    "-CreateDate=$EXIFDATE" \
    "-ModifyDate=$EXIFDATE" \
    "-TrackCreateDate=$EXIFDATE" \
    "-TrackModifyDate=$EXIFDATE" \
    "-MediaCreateDate=$EXIFDATE" \
    "-MediaModifyDate=$EXIFDATE" \
    "$FILE"

if [ $? -ne 0 ]; then
    echo "Error: exiftool failed"
    exit 1
fi

# Change filesystem timestamps
touch -t "$TOUCHDATE" "$FILE"

if [ $? -ne 0 ]; then
    echo "Error: touch failed"
    exit 1
fi

MKV remove dates

mkvpropedit input.mkv --edit info --delete date

Mazewar (1973) websocket V2


UPDATE: 20280826 – Made a Microcontroller joystick controller for the maze game.

Above test was abandoned, it was working okay.
But I made a V2 using some vibe coding … yeah, I know …
Time to shelf …

Works:

  • Mqtt websocket
  • Multiuser
  • Keyboard control
  • Fire/kill detection
  • Admin view

No working / needs work / nice to have:

  • Restart after kill, resets score
  • Set own username
  • Sound effects?
  • Degrees are > 360 .. lol

UPDATE: 20280828

import time
import board
import digitalio
import usb_hid

from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode


# GPIO -> tactile switch -> GND
BUTTONS = {
    board.GP2: Keycode.UP_ARROW,
    board.GP4: Keycode.DOWN_ARROW,
    board.GP3: Keycode.LEFT_ARROW,
    board.GP1: Keycode.RIGHT_ARROW,
    board.GP0: Keycode.SPACE,
    board.GP5: Keycode.Q,
    board.GP7: Keycode.E,
}


keyboard = Keyboard(usb_hid.devices)

inputs = {}

for pin, key in BUTTONS.items():
    button = digitalio.DigitalInOut(pin)
    button.direction = digitalio.Direction.INPUT
    button.pull = digitalio.Pull.UP
    inputs[pin] = (button, key)


# Remember previous state
last_state = {}

for pin, (button, key) in inputs.items():
    last_state[pin] = button.value

while True:

    for pin, (button, key) in inputs.items():

        state = button.value

        # Button changed
        if state != last_state[pin]:

            if not state:
                # Press
                keyboard.press(key)

            else:
                # Release
                keyboard.release(key)

            last_state[pin] = state

    time.sleep(0.005)

Cheap yellow display

I’ve posted about these a while ago. (OpenHASP)

I bought some new ones.
These will be used for serial logging, plotting data and terminal emulation for a 6502 system.

I’ve got some M5Stack credit card sized keyboards, so next to do is implement these instead of using the onscreen keyboard.

Flashing a new ROM on an Android tablet.

It has been a long time, since I flashed the last one.
Some things have changed.

I’ve flashed tablets and phones, now a tablet again.

The small tablet was really slow, and could not be upgraded to support a new webkit version.

So I installed a minimal Lineage OS on it.

Now it can be used as a immich web kiosk.

Some notes for myself

adb reboot bootloader
adb fastboot oem unlock
fastboot flash recovery twrp-3.7.0_9-1-flo_followmsi.img # this didn´t work, needed an older version
twrp-3.3.0-0-flo.img worked
adb push lanchon-repit-20210221-system=max-cache=16M+wipe-flo.zip /tmp/lanchon-repit-20210221-system=max-cache=16M+wipe-flo.zip
also lineage-20.0-20230830-UNOFFICIAL-flo.zip
adb install -r F-Droid.apk
adb install -r Rotation+Control_1.1_APKPure.apk # This because trebuchet rotate is broken.

While waiting for flashing, I installed nerdfonts, and patched them with cool icons. Then I installed superfile, a terminal file manager. Nice!

Dual PICO UART test with CirquitPython

Test for UART serial communication for my whack-a-mole game

CODE for Client

import board
import busio
import time

uart = busio.UART(
    tx=board.GP16,
    rx=board.GP17,
    baudrate=115200,
    timeout=1,
)

time.sleep(2)

commands = ["PING", "HELLO", "TEST"]
while True:
    for cmd in commands:
        print("Sending:", cmd)
        uart.write((cmd + "\n").encode())

        response = uart.readline()

        if response:
            print("Response:", response.decode().strip())
        else:
            print("No response")

        time.sleep(1)

CODE for server

import board
import busio
import digitalio
import time

# UART pins
uart = busio.UART(
    tx=board.GP0,
    rx=board.GP1,
    baudrate=115200,
    timeout=0.1
)

# Onboard LED
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

print("UART LED server ready")

while True:
    data = uart.readline()

    if data:
        command = data.decode("utf-8").strip()
        print("Received:", command)

        if command == "PING":
            # Blink LED
            led.value = True
            time.sleep(0.2)
            led.value = False

            # Reply
            uart.write(b"PONG\n")

        else:
            uart.write(b"UNKNOWN\n")

    time.sleep(0.01)

New CO2 sensor

Today I got, amongst other goodies a new CO2 sensor in the mail.

This is a MH-Z19C CO2/Temperature sensor.
(Easy to implement with ESPHome)

Calibrate using HD 7 seconds to GND
(Do this in a well ventilated room)

YAML Code

# Board: Wemos D1 Mini (ESP8266) (Wemos)
# Definition: definitions/boards/d1_mini/manifest.yaml

esphome:
  name: newco2
  friendly_name: NewCO2

esp8266:
  board: d1_mini

logger:

api:
  encryption:
    key: "c/vSFcxi1YgYlXtvsjXXXXXXXXXXXXXXXXXXXXXX="

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: NewCO2 Fallback Hotspot
    password: "tepe9XXXXXXXXXXX"

captive_portal:

uart:
  - baud_rate: 9600
    rx_pin: 4
    tx_pin: 5
    id: uart_1

sensor:
  - platform: mhz19
    uart_id: uart_1
    co2:
      id: sensor_mhz19_1_co2
      name: CO2
    temperature:
      id: sensor_mhz19_1_temperature
      name: Temperature
    id: sensor_mhz19_1
    automatic_baseline_calibration: true
    detection_range: "5000ppm"
    
switch:
  - platform: template
    name: "MH-Z19 ABC"
    optimistic: true
    on_turn_on:
      mhz19.abc_enable: sensor_mhz19_1
    on_turn_off:
      mhz19.abc_disable: sensor_mhz19_1

light:
  - platform: status_led
    name: status
    id: light_status_led_1
    pin: GPIO12

Blender plugin for 8 potmeter input

This weekend I saw a corridor crew video about controlling CGI objects using input devices.

This made me think of a half finished project I did a while ago.
So lets finish it.

(I know, dirty screen .. too much outside hacking)

8Angle M5 thingy using I2C (Pin D1 and D2)

CODE for wemos

Very simple code to read pots using I2C and printing values on the serial output using the wemos.
Only needs 5V,GND,SDA,SCL (D1 D2)

279,206,520,1023,1023,60,300,985,0
Output example 8 potmeters 0-1023 value and last value 0 or 1 for the switch

Serial plotter example below

#include "m5angle8.h"

M5ANGLE8 MM;

void setup()
{
  Serial.begin(115200);

  Serial.println();
  delay(100);

  Wire.begin();
  MM.begin();
}


void loop()
{
  for (int ch = 0; ch < 8; ch++)
  {
    Serial.print(MM.analogRead(ch, 10));
    Serial.print(",");
    delay(1);
  }
  Serial.print(MM.inputSwitch());
  Serial.print("\n");
  delay(100);
}

Plugin for Blender

filename unit8angle_blender/__init__.py

made into a zip you can install using

zip -r myplugin.zip unit8angle_blender

Blender edit > preferences > add-ons > Install from disk

import bpy
import serial
import serial.tools.list_ports

ser = None


def open_serial():
    global ser

    if ser:
        return

    # Change this to your COM port
    ser = serial.Serial("/dev/ttyUSB0",115200,timeout=0)


class SERIAL_OT_start(bpy.types.Operator):
    bl_idname = "wm.unit8_start"
    bl_label = "Start Unit8"

    _timer = None

    def modal(self, context, event):

        if event.type == 'TIMER':

            global ser

            if ser and ser.in_waiting:

                line = ser.readline().decode(errors="ignore").strip()

                try:

                    values = list(map(int,line.split(",")))

                    if len(values) >= 8:

                        obj = context.active_object

                        if obj:

                            obj.location.x = values[0] / 100.0
                            obj.location.y = values[1] / 100.0
                            obj.location.z = values[2] / 100.0

#                            obj.location.x = values[0] / 1023.0
#                            obj.location.y = values[1] / 1023.0
#                            obj.location.z = values[2] / 1023.0

                            obj.rotation_euler.x = values[3] / 1023.0 * 6.28318
                            obj.rotation_euler.y = values[4] / 1023.0 * 6.28318
                            obj.rotation_euler.z = values[5] / 1023.0 * 6.28318

                            s = 0.1 + values[6] / 1023.0 * 3.0
                            obj.scale = (s,s,s)

                except Exception:
                    pass

        return {'PASS_THROUGH'}

    def execute(self, context):

        open_serial()

        wm = context.window_manager

        self._timer = wm.event_timer_add(0.01, window=context.window)

        wm.modal_handler_add(self)

        return {'RUNNING_MODAL'}


class SERIAL_PT_panel(bpy.types.Panel):
    bl_label = "Unit8Angle"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Unit8'

    def draw(self, context):
        self.layout.operator("wm.unit8_start")


classes = (
    SERIAL_OT_start,
    SERIAL_PT_panel,
)


def register():
    for c in classes:
        bpy.utils.register_class(c)


def unregister():
    for c in reversed(classes):
        bpy.utils.unregister_class(c)