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.
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).
#!/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()