Ubuntu Suomen keskustelualueet

Ubuntun käyttö => Ohjelmointi, palvelimet ja muu edistyneempi käyttö => Aiheen aloitti: kamara - 31.10.26 - klo:12.29

Otsikko: Fiiliskoodaus-tuotoksia...
Kirjoitti: kamara - 31.10.26 - klo:12.29
Ajattelin tehdä säikeen Vibe-koodaukselle eli koodeille, jotka ovat TÄ (teko älyn):n tuotoksia.

Toki noita löytyy itseltänikin monia, mutta tässä pari pientä makupalaa...
(puhelin)kameran perspektiivikorjaus kuville. Tuntee lähinnä muodot 1x1 ja 1x2, mutta toimii paremmin kuin löytämäni Anteron Apinoissa.

Eli ota kuva kännykällä/kameralla ja korjaa valokuvan vääristämät mittasuhteet oikeiksi. (Itse tarvitsin vain 1x1 ja 1x2 valokuvansuhteilla, joita tämä skripti tukenee.) Hiirellä tulee klikkailla kopioitavan kuvan kulmapisteet.
Koodia: [Valitse]
import sys
import cv2
import numpy as np

if len(sys.argv) != 4:
    sys.exit("Käyttö: python3 Manuaalinen.py sisään.jpg ulos.png ulosfix.png")

in_path, out_path = sys.argv[1], sys.argv[2]
img = cv2.imread(in_path)
if img is None:
    sys.exit(f"Kuvaa ei voitu avata: {in_path}")

H, W = img.shape[:2]
print(f"Alkuperäinen koko: {W}x{H}")

# skaalaus ruudulle sopivaksi (max 900 px)
scale = min(1.0, 900 / max(W, H))
small = cv2.resize(img, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA)

pts = []
win = "Klikkaa 4 kulmaa | Backspace = peru | Enter = valmis | Esc = lopeta"

def order_points(p):
    p = np.array(p, dtype="float32")
    s = p.sum(axis=1)
    d = np.diff(p, axis=1).ravel()
    return np.array([p[np.argmin(s)], p[np.argmin(d)],
                     p[np.argmax(s)], p[np.argmax(d)]], dtype="float32")

def redraw():
    view = small.copy()
    for i, (x, y) in enumerate(pts):
        cv2.circle(view, (x, y), 6, (0, 0, 255), -1)
        cv2.putText(view, str(i + 1), (x + 8, y - 8),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    if len(pts) == 4:
        o = order_points(pts).astype(int)
        cv2.polylines(view, [o.reshape(-1, 1, 2)], True, (0, 255, 0), 2)
    cv2.imshow(win, view)

def click(e, x, y, flags, param):
    if e == cv2.EVENT_LBUTTONDOWN and len(pts) < 4:
        pts.append([x, y])
        redraw()

cv2.namedWindow(win)
cv2.setMouseCallback(win, click)
redraw()

while True:
    k = cv2.waitKey(20) & 0xFF
    if k == 27:
        sys.exit("Peruttu")
    if k == 8 and pts:          # Backspace
        pts.pop()
        redraw()
    if k in (10, 13) and len(pts) == 4:   # Enter
        break
cv2.destroyAllWindows()

src = order_points(pts) / scale
print("Kulmat alkuperäisessä kuvassa:\n", src)

# lähtökoko: keskimääräinen sivun pituus
side = int(np.mean([np.linalg.norm(src[i] - src[(i + 1) % 4]) for i in range(4)]))
dst = np.array([[0, 0], [side-1, 0], [side-1, side-1], [0, side-1]], dtype="float32")

M = cv2.getPerspectiveTransform(src, dst)
out = cv2.warpPerspective(img, M, (side, side))
cv2.imwrite(out_path, out)
print("Tallennettu:", out_path, f"({side}x{side})")


src = order_points(pts) / scale
print("Kulmat alkuperäisessä kuvassa:\n", src)

# ---------- kuvasuhteen arviointi ----------
# Sallitut "siistit" suhteet (leveys/korkeus). Lisää tähän vapaasti omia.
CANDIDATES = [1, 2, 3, 1/2, 1/3]
TOLERANCE = 0.40   # snap vain jos arvio on alle 15 % päässä

def estimate_ratio_zhang_he(c, W, H):
    """Zhang & He: kuvasuhde nelikulmion kulmista, polttoväli ratkaistaan itse.
    c = [vasen ylä, oikea ylä, oikea ala, vasen ala]. Palauttaa None jos epäonnistuu."""
    tl, tr, br, bl = c
    m1, m2, m3, m4 = [np.array([x, y, 1.0]) for x, y in (tl, tr, bl, br)]
    try:
        k2 = np.dot(np.cross(m1, m4), m3) / np.dot(np.cross(m2, m4), m3)
        k3 = np.dot(np.cross(m1, m4), m2) / np.dot(np.cross(m3, m4), m2)
    except ZeroDivisionError:
        return None
    n2 = k2 * m2 - m1
    n3 = k3 * m3 - m1
    u0, v0 = W / 2, H / 2
    den = n2[2] * n3[2]
    if abs(den) < 1e-9:
        return None      # lähes suoraan edestä kuvattu -> ei perspektiiviä
    f2 = -1 / den * (
        (n2[0]*n3[0] - (n2[0]*n3[2] + n2[2]*n3[0]) * u0 + den * u0**2) +
        (n2[1]*n3[1] - (n2[1]*n3[2] + n2[2]*n3[1]) * v0 + den * v0**2))
    if f2 <= 0:
        return None
    f = np.sqrt(f2)
    if not (0.3 * max(W, H) < f < 5 * max(W, H)):
        return None      # epärealistinen polttoväli
    A = np.array([[f, 0, u0], [0, f, v0], [0, 0, 1]])
    Ai = np.linalg.inv(A)
    B = Ai.T @ Ai
    num, d = n2 @ B @ n2, n3 @ B @ n3
    if num <= 0 or d <= 0:
        return None
    r = np.sqrt(num / d)
    return r if 0.1 < r < 10 else None

def estimate_ratio_simple(c):
    top = np.linalg.norm(c[1] - c[0]); bottom = np.linalg.norm(c[2] - c[3])
    left = np.linalg.norm(c[3] - c[0]); right = np.linalg.norm(c[2] - c[1])
    return ((top + bottom) / 2) / ((left + right) / 2)

ratio = estimate_ratio_zhang_he(src, W, H)
if ratio is None:
    ratio = estimate_ratio_simple(src)
    print(f"Perspektiivimenetelmä ei onnistunut, käytetään sivujen pituuksia: {ratio:.3f}")
else:
    print(f"Arvioitu kuvasuhde (leveys/korkeus): {ratio:.3f}")

best = min(CANDIDATES, key=lambda c: abs(np.log(ratio / c)))
if abs(np.log(ratio / best)) < np.log(1 + TOLERANCE):
    print(f"Pyöristetään lähimpään suhteeseen: {best:.3f}")
    ratio = best
else:
    print("Ei lähellä mitään tuttua suhdetta, käytetään mitattua arvoa.")

# ---------- ulostulon koko ----------
# säilytetään suunnilleen sama pikselimäärä kuin kohteella oli kuvassa
w_est = (np.linalg.norm(src[1] - src[0]) + np.linalg.norm(src[2] - src[3])) / 2
h_est = (np.linalg.norm(src[3] - src[0]) + np.linalg.norm(src[2] - src[1])) / 2
area = w_est * h_est
out_w = int(round(np.sqrt(area * ratio)))
out_h = int(round(np.sqrt(area / ratio)))

dst = np.array([[0, 0], [out_w-1, 0], [out_w-1, out_h-1], [0, out_h-1]], dtype="float32")
M = cv2.getPerspectiveTransform(src, dst)
out = cv2.warpPerspective(img, M, (out_w, out_h))
out_path="fix-"+out_path
out_path=sys.argv[3]
cv2.imwrite(out_path, out)
print("Tallennettu:", out_path, f"({out_w}x{out_h})")

Meater+ paistomittarin arvot tietokoneelle:
Tukee hälytystä tip(kärjen)-lämpötilan mukaan ja antaa kaunista käppyrää. Huomioitavaa, että MAC-koodi on poistettu ja kyseinen mittarin arvon korjaus on todennäköisesti mittari-kohtainen, mutta se ei todennäköisesti kovinkaan paljoa vääristä tuloksia, kun minun mittarini (tarvitsema)korjaus on (käsittääkseni) alle asteen (keskimäärin).
Koodia: [Valitse]
#!/usr/bin/python3
import argparse
import asyncio
import shutil
import subprocess
import sys
import threading
import time

import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from bleak import BleakClient
from matplotlib.animation import FuncAnimation

# --- argumentit ---
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--alarm", type=float, metavar="LÄMPÖTILA",
                    help="hälytysraja tip-lämpötilalle (°C)")
args = parser.parse_args()

SOUND = "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"

def beep():
    if shutil.which("paplay"):
        subprocess.Popen(["paplay", SOUND])
    else:
        print("\a", end="", flush=True)

# --- Meater ---
MAC = "xx:xx:xx:xx:xx:xx"
TEMP_UUID = "7edda774-045e-4bbf-909b-45d1991a2876"
BATTERY_UUID = "2adb4877-68d8-4884-bd3c-d83853bf27b8"
AMBIENT_K = 2.545

def calibrate_meater(t):
    a = 1.0116
    b = -0.24
    return a * t + b

def decode_temp(payload):
    tip_raw = int.from_bytes(payload[0:2], "little", signed=True)
    ambient_raw = int.from_bytes(payload[2:4], "little", signed=True)
    ambient_offset = int.from_bytes(payload[4:6], "little", signed=True)
    return tip_raw / 16.0, (ambient_raw + ambient_offset) / AMBIENT_K

def decode_battery(payload):
    return int.from_bytes(payload[0:2], "little")

# --- jaettu data ---
lock = threading.Lock()
times, tips, ambients = [], [], []
start = time.monotonic()

def temp_callback(sender, data):
    tip, ambient = decode_temp(data)
    tip = calibrate_meater(tip)
    t = time.monotonic() - start
    with lock:
        times.append(t)
        tips.append(tip)
        ambients.append(ambient)
    print(f"Tip: {tip:.2f} °C | Ambient: {ambient:.2f} °C | time: {t:.2f}",
          file=sys.stderr, flush=True)

async def ble_main():
    async with BleakClient(MAC) as client:
        battery = decode_battery(await client.read_gatt_char(BATTERY_UUID))
        print(f"Battery: {battery}%", file=sys.stderr, flush=True)
        await client.start_notify(TEMP_UUID, temp_callback)
        while True:
            await asyncio.sleep(1)

def ble_thread():
    try:
        asyncio.run(ble_main())
    except Exception as e:
        print(f"BLE error: {e!r}", file=sys.stderr, flush=True)

threading.Thread(target=ble_thread, daemon=True).start()

# --- graafi ---
fig, ax = plt.subplots()
line_tip, = ax.plot([], [], label="Tip")
line_amb, = ax.plot([], [], label="Ambient")
if args.alarm is not None:
    ax.axhline(args.alarm, color="red", linestyle="--", linewidth=1)
ax.set_xlabel("Aika (s)")
ax.set_ylabel("Lämpötila (°C)")
ax.grid(True)
ax.legend(loc="lower right")

info = ax.text(0.02, 0.97, "", transform=ax.transAxes, va="top",
               family="monospace", fontsize=12)
big = ax.text(0.02, 0.92, "", transform=ax.transAxes, va="top", ha="left",
              family="monospace", fontsize=96, fontweight="bold",
              bbox=dict(facecolor="white", alpha=0.7, edgecolor="none"))

alarm_active = False
last_beep = 0.0

def update(_):
    global alarm_active, last_beep
    with lock:
        t, tp, am = list(times), list(tips), list(ambients)
    if not t:
        return line_tip, line_amb, info, big

    line_tip.set_data(t, tp)
    line_amb.set_data(t, am)
    ax.relim()
    ax.autoscale_view()
    if args.alarm is not None:
        lo, hi = ax.get_ylim()
        ax.set_ylim(lo, max(hi, args.alarm + 5))

    info.set_text(f"t = {t[-1]:.1f} s")
    big.set_text(f"{tp[-1]:.1f} °C")

    if args.alarm is not None and tp[-1] >= args.alarm:
        now = time.monotonic()
        if not alarm_active:
            alarm_active = True
            print(f"ALARM: Tip {tp[-1]:.1f} °C >= {args.alarm} °C "
                  f"(t = {t[-1]:.1f} s)", file=sys.stderr, flush=True)
        big.set_color("red")
        ax.set_facecolor("#ffcccc" if int(now * 2) % 2 else "white")
        if now - last_beep > 3:
            beep()
            last_beep = now
    else:
        alarm_active = False
        big.set_color("black")
        ax.set_facecolor("white")
    return line_tip, line_amb, info, big

ani = FuncAnimation(fig, update, interval=500, cache_frame_data=False)
mng = plt.get_current_fig_manager()
mng.window.attributes("-zoomed", True)
plt.show()