Skip to main content
Uygulama Geliştirme•21 dk okuma

Kamera Pikselini CNC Koordinatına Çevirmek: Kalibrasyon, Homografi ve Modbus

Mustafa Kürşad BaşerMustafa Kürşad BAŞER
•
Kamera Pikselini CNC Koordinatına Çevirmek: Kalibrasyon, Homografi ve Modbus

Pikselden takıma dört dönüşüm, dört ayrı hata

Özet (TL;DR)

  • calibrateCamera'nın döndürdüğü RMS, modelin kendi verisine uyumunu ölçer, doğruluğunu değil. Tahta hep görüntünün ortasında kalırsa 0,17 piksellik RMS ile kenarda onlarca milimetre yanılan bir model çıkabiliyor.
  • Çalışma yüzeyi düz bir tablaysa piksel ile milimetre arasında tek bir homografi yeter. Şartı önce lens bozulmasını atmak: örnek kurulumda bu adımı atlamak ortalama hatayı 0,09 mm'den 5,9 mm'ye çıkarıyor.
  • Homografi yalnızca hesaplandığı düzlemde doğrudur. 754 mm yükseklikteki kamerada tablanın 3 mm üstündeki yüzey 1,4 mm'ye varan kayma demek.
  • Kamera ayağının 0,1 derece eğilmesi tablanın her yerinde 1,35 mm hata üretir. Hassasiyetin sınırını çoğu zaman piksel değil, montajın açısal kararlılığı çizer.
  • Modbus yalnızca 16 bitlik register'ın bayt sırasını tanımlar, 32 bitlik koordinatın iki register'a nasıl bölüneceği cihaza kalmıştır. Dört baytı birbirinden farklı bir test değeriyle doğrulamadan makineye koordinat yollamayın.

Diyelim ki görüntüde kesim çizgisinin başladığı nokta (812, 344) pikselinde. Makine pikselden anlamaz, ona X = 214,30 ve Y = 97,85 gibi milimetre cinsinden bir hedef lazım. Görüntünün ortasında her şey yerine oturuyor, tablanın kenarına doğru takım çizgiden birkaç milimetre kaçıyor. Üstüne PLC'nin ekranında X değeri −1,88 × 10²⁵ görünüyor.

StitchAI'da kamera karesinden CNC'ye giden hat tam bu noktadan başlıyor. Basler kameradan gelen kare 4 noktalı bir perspektif matrisiyle gerçek dünya koordinatlarına çevriliyor, üretilen program da Modbus RTU ya da TCP üzerinden makineye gidiyor. Kâğıt üstünde iki adım gibi duruyor. Arada en az dört dönüşüm var ve her biri hataya kendi payını ekliyor.

Her adımın hatasını ölçebilmek için iç parametrelerini bildiğim sentetik bir kamera kullandım: 1280x960 piksel, 1400 piksellik odak uzaklığı, belirgin fıçı bozulması (k1 = −0,28). Böylece bulunan her değeri gerçeğiyle karşılaştırabiliyorum. Kodlar Ocak 2025'te çıkan OpenCV 4.11.0, NumPy 2.2 ve pymodbus 3.8.6 ile çalıştı.

Kameranın iki tür parametresi

İğne deliği (pinhole) kamera modelinde bir dünya noktası şu zincirle piksele iner: s · [u, v, 1]ᵀ = K · [R | t] · [X, Y, Z, 1]ᵀ. [R | t] dış (extrinsic) parametrelerdir, kameranın dünyaya göre nerede durduğunu ve nereye baktığını söyler. K iç (intrinsic) parametre matrisidir: piksel cinsinden odak uzaklığı (fx, fy) ve optik eksenin sensörü deldiği nokta (cx, cy). Gerçek lensler bu modele uymadığı için yanına radyal (k1, k2, k3) ve teğetsel (p1, p2) bozulma (distortion) katsayıları eklenir.

Bu ayrım bakım takvimini belirliyor. İç parametreler lense ve sensöre aittir, odak ya da diyafram halkasına dokunulmadıkça değişmez ve bir kez dikkatle ölçülür. Dış parametreler montaja aittir, ayaktaki bir vida yarım tur gevşediğinde değişir. Birini laboratuvarda ölçersiniz, diğerini sahada ve sık sık.

Satranç tahtasıyla kalibrasyon

OpenCV'nin calibrateCamera fonksiyonu Zhang'ın 2000 tarihli yöntemine dayanıyor. Düz bir desenin farklı açılardan çekilmiş her görüntüsü için bir homografi çıkarılıyor, iç parametreler bu homografilerin kısıtlarından kestiriliyor ve sonra hepsi birlikte doğrusal olmayan en küçük karelerle iyileştiriliyor. Size köşeleri bulmak kalıyor.

python
import cv2import numpy as np # goruntuler: gri tonlu kalibrasyon kareleri (14 adet, 1280x960)KOSE, KARE_MM = (9, 6), 20.0  # iç köşe sayısı, kare kenarıobjp = np.zeros((KOSE[0] * KOSE[1], 3), np.float32)objp[:, :2] = np.mgrid[0:KOSE[0], 0:KOSE[1]].T.reshape(-1, 2) * KARE_MM  # Z = 0kriter = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) nesne, ham, ince = [], [], []for gri in goruntuler:    bulundu, kose = cv2.findChessboardCorners(gri, KOSE, None)    if not bulundu:        continue    nesne.append(objp)    ham.append(kose.copy())    ince.append(cv2.cornerSubPix(gri, kose, (11, 11), (-1, -1), kriter)) boyut = goruntuler[0].shape[::-1]  # (genişlik, yükseklik)for ad, noktalar in (("cornerSubPix yok", ham), ("cornerSubPix var", ince)):    rms, K, D, rv, tv = cv2.calibrateCamera(nesne, noktalar, boyut, None, None)    print(f"{ad:17s} RMS = {rms:.3f} px") # Çıktı:# cornerSubPix yok  RMS = 0.203 px# cornerSubPix var  RMS = 0.151 px

Tahta koordinatlarında Z = 0 ve birim milimetre. Kare kenarını yanlış girerseniz K etkilenmez ama tahtaya dayanan her milimetre hesabı etkilenir. Basılmış tahtanın karelerini kumpasla ölçün, yazıcılar ölçeği sessizce değiştirebiliyor.

cornerSubPix köşeyi gri seviye gradyanlarına bakarak piksel altı bir konuma taşıyor. Sentetik görüntülerde gerçek köşeyi bildiğim için etkisini doğrudan ölçebildim: findChessboardCorners tek başına gerçek köşeden ortalama 0,18 piksel sapıyor, cornerSubPix sonrasında sapma 0,13 piksele iniyor.

RMS düşük diye kalibrasyon doğru değildir

calibrateCamera'nın döndürdüğü sayı, yeniden izdüşüm hatasının (reprojection error) karekök ortalaması. Tahta köşelerinin görüntüdeki izdüşümü bulunan parametrelerle yeniden hesaplanıyor ve tespit edilen köşelerle aradaki fark ölçülüyor. Modelin görmediği bölgede ne yapacağı bu sayıda yok. Aynı kamerayı iki veri setiyle kalibre ettim:

VeriRMSfx (gerçek 1400)k1 (gerçek −0,28)14 görüntünün hepsinde hata(60, 60) pikselinde, 500 mm'de sapma
14 görüntü, tahta görüntünün her yerinde0,151 px1400,7−0,2810,123 px0,29 mm
3 görüntü, tahta hep ortada0,168 px1400,0−0,2481,440 px55,21 mm

İki kalibrasyonun RMS'i neredeyse aynı. İkincisinde tahta kenara hiç gitmediği için bozulma polinomu kenarda serbest kalmış ve görüntü köşesine yakın bir piksel 500 mm ötede 55 mm yanlış yere düşüyor. Tahtayı görüntünün dört köşesine götürün, eğin, OpenCV'nin önerdiği gibi en az on görüntü toplayın. Görüntü başına RMS'i diğerlerinden belirgin yüksek olan kareyi atın, ya bulanıktır ya da köşeleri yanlış eşleşmiştir.

Bir tuzak daha var. OpenCV'nin Python kalibrasyon tutorial'ındaki "total error" her görüntü için L2 normunu nokta sayısına bölüyor. Bu RMS değil, nokta sayısının karekökü kadar daha küçük bir sayı. Aynı veride tutorial'daki formül 0,019, calibrateCamera 0,151 veriyor.

Lens bozulmasını atmak

Fıçı bozulmasında düz çizgiler kenara doğru dışa bombe yapar ve etki merkezden uzaklaştıkça büyür. cv2.undistort bozulmayı tek çağrıda giderir, ama belgelerin söylediği gibi initUndistortRectifyMap ile remap'in birleşimidir ve haritayı her çağrıda yeniden hesaplar. Kameradan sürekli kare geliyorsa haritayı bir kez kurun:

python
# K, D: calibrateCamera çıktısı, kare: kameradan gelen gri görüntü, boyut = (1280, 960)yeniK, roi = cv2.getOptimalNewCameraMatrix(K, D, boyut, alpha=0)harita1, harita2 = cv2.initUndistortRectifyMap(K, D, None, yeniK, boyut, cv2.CV_16SC2) duz = cv2.remap(kare, harita1, harita2, cv2.INTER_LINEAR)  # her karede yalnızca bu satır # Çıktı:# ham kare      : üst sıradaki köşelerin doğrudan en büyük sapması 1.38 px# remap sonrası : 0.10 px

Tahta görüntünün sol üst köşesindeyken üst sıradaki dokuz köşe ham karede düz çizgiden 1,38 piksel sapıyor, düzeltilmiş karede 0,10. Aynı bilgisayarda undistort kare başına remap'ten yedi kattan fazla yavaş çalıştı. alpha = 0 siyah kenar bırakmaz ama kenardan piksel kırpar, alpha = 1 bütün pikselleri tutar. Hangisini seçerseniz seçin, sonraki her hesap yeniK'nın piksel uzayında yapılmalı.

Çift düzeltme tuzağı

Bütün kareyi düzeltmek zorunda değilsiniz. Kontur ya da köşe ham karede bulunuyorsa yalnızca o noktaları cv2.undistortPoints(..., P=K) ile düzeltmek hem ucuzdur hem de enterpolasyon kaybı getirmez. Sorun ikisini birden yapınca çıkar. Düzeltilmiş karede bulunan noktayı bir de undistortPoints'ten geçirirseniz bozulmayı iki kez çıkarır, kenarlarda ters yönde bir bozulma elde edersiniz. Hattın her yerinde "bu piksel hangi uzayda" sorusunun tek cevabı olmalı.

Tabla bir düzlemse homografi yeter

Tablanın üstündeki her nokta için Z = 0. Bunu izdüşüm denklemine koyunca R'nin üçüncü sütunu düşüyor ve geriye 3x3'lük bir matris kalıyor: H = K · [r1 r2 t]. Ölçeği keyfî olduğundan sekiz serbestlik derecesi var, her nokta eşleşmesi iki denklem verir. Bu yüzden en az dört nokta gerekir ve üçü aynı doğru üzerinde olmamalı.

Tam 3B kalibrasyona gerek yok, çünkü sorulan soru yalnızca "bu piksel tablanın neresine düşüyor". KISS ilkesinin buradaki karşılığı: problemin istemediği serbestlik derecesini modele koymayın. Tek istisna lens bozulması. Homografi doğruyu doğruya taşır, bükülmüş çizgiyi düzeltemez.

python
def piksel_to_mm(px, H, K=None, D=None):    pts = np.asarray(px, np.float64).reshape(-1, 1, 2)    if K is not None:        pts = cv2.undistortPoints(pts, K, D, P=K)  # bozulmayı at, yine piksel biriminde kal    return cv2.perspectiveTransform(pts, H).reshape(-1, 2)  ref_mm = np.array([[40, 40], [620, 40], [620, 460], [40, 460]], np.float64)ref_px = piksel(ref_mm)  # işaretlerin görüntüdeki merkezleri (sentetik ölçüm, 0,1 px gürültü) H_ham, _ = cv2.findHomography(ref_px, ref_mm)  # bozulma hesaba katılmadanref_duz = cv2.undistortPoints(ref_px.reshape(-1, 1, 2), K, D, P=K)H_duz, _ = cv2.findHomography(ref_duz, ref_mm)  # önce undistort

Bu örnekte kamera tablaya 754 mm yükseklikten, birkaç derece eğik bakıyor ve merkezde bir piksel 0,54 mm ediyor. Referanslar 580 x 420 mm'lik bir dikdörtgenin köşelerinde. Hatayı 10 mm aralıklı 3.417 test noktasında, bir önceki adımda kestirilen bozulma katsayılarıyla ölçtüm:

KurulumOrtalama hataReferans dikdörtgeni içinde en büyükTablanın tamamında en büyük
4 nokta, undistort yok5,89 mm8,42 mm8,42 mm
4 nokta, önce undistort0,09 mm0,14 mm0,41 mm
35 nokta (7x5), önce undistort0,05 mm0,19 mm0,48 mm

İlk satır en sık yapılan hata. Dört nokta homografiyi tam belirler, referans noktalarında hata sıfırdır, aralarında ise bozulma ne kadarsa o kadar. Nokta sayısını 35'e çıkarmak ortalamayı yarıya indiriyor ama en büyük hatayı düşürmüyor. O hata referans dikdörtgeninin dışında ve kalibrasyonun kenardaki kalitesine bağlı. Referans işaretlerini çalışma alanının en dış köşelerine koyun. Yanlış eşleşme ihtimali olan çok sayıda noktanız varsa findHomography'ye cv2.RANSAC verin.

Homografinin bilmediği şey: yükseklik

Homografi yalnızca hesaplandığı düzlemde doğrudur. Tablanın h kadar üstündeki bir nokta, kameradan bakınca tabladaki başka bir noktanın önünde durur. Kameranın tam altından r uzaktaki noktada kayma yaklaşık r · h / (Z − h), Z kameranın tablaya yüksekliği. Bu kurulumda 3 mm yukarıdaki yüzey ortalama 0,91 mm, en fazla 1,37 mm kayıyor. Yüzey 10 mm yukarıdaysa ortalama 2,99 mm, en fazla 5,23 mm.

Kalın malzemede referansları malzemenin üst yüzeyinde ölçün. Kalınlık işten işe değişiyorsa kamera merkezini solvePnP ile bir kez bulup kaymayı bu formülle düzeltin.

Ham pikselden Modbus register'ına beş adımlık dönüşüm hattı. Her adımın yanında, o adım atlandığında ya da yanlış yapıldığında örnek kurulumda ölçülen hata yazıyor.
Dönüşüm hattı ve her adımın atlandığında getirdiği hata

Hata bütçesi

Piksel başına milimetre, ulaşabileceğiniz hassasiyetin sınırı değil. Köşe ve kenar tespiti piksel altında çalışır, iyi bir kalibrasyonla piksel boyutunun onda biri civarında tutarlılık elde edilebilir. Asıl sınırı mekanik çiziyor. Aynı sentetik kurulumda her kaynağın payı şöyle:

KaynakBu kurulumda etkisiNe yapılır
Piksel boyutuMerkezde 0,54 mm/pxÇözünürlük ya da görüş alanı seçimi
Köşe tespiti0,18 px, cornerSubPix ile 0,13 px (yaklaşık 0,07 mm)cornerSubPix ya da findChessboardCornersSB
Kalibrasyonun kenar hatasıTabla kenarında 0,41–0,48 mmTahtayı görüntünün köşelerine götürmek
Bozulmayı atlamak8,42 mm'ye kadarHomografiden önce undistort
3 mm malzeme kalınlığı1,37 mm'ye kadarReferansı malzeme yüzeyinde ölçmek
Kamera ayağının 0,1° eğilmesiOrtalama 1,35 mmRijit montaj, referans kontrolü
Kameranın optik eksen etrafında 0,1° dönmesiOrtalama 0,34 mmReferans kontrolü
Kameranın 0,2 mm yana kayması0,20 mmReferans kontrolü
754 mm'lik alüminyum ayağın 10 K ısınmasıYaklaşık 0,17 mm uzama, en fazla 0,08 mm hataÇoğu kurulumda ihmal edilebilir

Tablonun en çarpıcı satırı açı. Kamera ayağının 0,1 derece eğilmesi tablanın her yerinde 1,35 mm kayma demek. Alüminyumun ısıl genleşme katsayısı yaklaşık 23 µm/(m·K), yani aynı ayak 10 derece ısınınca yalnızca 0,17 mm uzuyor. Titreşimin asıl zararı da açıda: bağlantıları zamanla gevşetip açıyı yavaş yavaş değiştirebilir.

Pratik çözüm kalibrasyonu sürekli doğrulamak. Tablaya, çalışma alanının dışında kalan ve hiç sökülmeyen üç dört referans işareti koyun. Her işe başlarken bu işaretlerin piksel konumlarını kayıtlı değerlerle karşılaştırın, fark eşiği aşıyorsa homografiyi yeniden hesaplayın ya da operatörü uyarın. StitchAI'da hizası kaymış kamera, kötü ışık ve kontrast sorunları operatöre ayrı birer görsel geri bildirimle gösteriliyor.

Makinenin koordinat sistemine hizalamak

Homografinin verdiği milimetreler referans işaretlerinin çerçevesindedir. Makinenin çerçevesinde orijin başka bir köşede olabilir, bir eksen ters yöne bakabilir, kontrolör milimetre yerine mikron bekleyebilir. Görüntüde v ekseni aşağı doğru artar, makinenin Y ekseni ters yönde olabilir. Bu farkı 2B bir dönüşümle kapatacaksanız dönüşümün türü önemli:

python
# Makineyi dört işarete sürüp okunan konumlar (makine mm) ve aynı işaretlerin tabla mm'sitabla = np.array([[40, 40], [620, 40], [620, 460], [40, 460]], np.float64)makine = tabla_to_makine(tabla)  # sentetik: Y ters, orijin kaymış, 0,4 derece dönük A_benzer, _ = cv2.estimateAffinePartial2D(tabla, makine)  # döndürme + ölçek + ötelemeA_afin, _ = cv2.estimateAffine2D(tabla, makine)            # yansımaya da izin verir for ad, A in (("estimateAffinePartial2D", A_benzer), ("estimateAffine2D", A_afin)):    tahmin = tabla @ A[:, :2].T + A[:, 2]    hata = np.linalg.norm(tahmin - makine, axis=1).max()    print(f"{ad:24s} det = {np.linalg.det(A[:, :2]):+.4f}  maks hata = {hata:.2f} mm") # Çıktı:# estimateAffinePartial2D  det = +1.0000  maks hata = 840.00 mm# estimateAffine2D         det = -1.0000  maks hata = 0.00 mm

estimateAffinePartial2D döndürme, tekdüze ölçek ve öteleme arıyor. Yansıma bu ailede yok, üstelik varsayılan yöntem RANSAC olduğu için dört noktanın ikisine uyup kalanları 840 mm ıskalayan bir çözümü hata vermeden döndürüyor. estimateAffine2D yansımayı da kapsıyor ve determinantın −1 çıkması eksenlerden birinin ters olduğunu açıkça söylüyor.

Ben daha kısa yolu tercih ederim: homografiyi doğrudan makine koordinatlarına karşı kurmak. Makineyi tabladaki dört ya da daha fazla işarete sürün, okunan konumları findHomography'ye hedef olarak verin. Orijin, eksen yönü, ölçek ve kameranın eğikliği tek matrise girer, elle yazılmış bir eksen çevirme satırı kalmaz. Sonucu kurulumda kullanmadığınız beşinci bir noktada doğrulayın.

Orijini satranç tahtasının ilk köşesinden alıyorsanız dikkat edin. Aynı 14 görüntüde findChessboardCornersSB köşeleri findChessboardCorners'a göre ters sırayla döndürdü, tahtanın orijini 180 derece döndü. Fonksiyon değiştirirken ilk köşenin yerini kontrol edin.

Koordinatı register'lara yazmak

Modbus'ın veri modeli 16 bitlik register'lardan oluşur ve spesifikasyon bunların taşınmasını açıkça yazar: adresler ve veriler big-endian gönderilir, 0x1234 için önce 0x12 gider. 32 bitlik tamsayıdan ya da float'tan hiç söz etmez. Bir koordinatı iki register'a bölmek cihaz üreticisine kalmıştır ve üreticiler farklı yollar seçmiştir.

pymodbus 3.8'de bu iş için convert_to_registers ve convert_from_registers var, ikisi de bağlantı açmadan çağrılabilen sınıf metotları. Eski BinaryPayloadBuilder ile BinaryPayloadDecoder 3.8.0'da deprecated olarak işaretlendi, 3.8.6'nın kaynağı 3.9.0'da gideceklerini yazıyor. word_order parametresi ise 3.8.1'de geldi. Bu hızda değişen bir kütüphaneyi pymodbus==3.8.6 gibi tam sabitleyin. Semantik versiyonlama sözleşmesine her proje aynı sıkılıkla uymuyor.

python
from pymodbus.client import ModbusTcpClient DT = ModbusTcpClient.DATATYPE x_mm = 123.456f32 = ModbusTcpClient.convert_to_registers(x_mm, DT.FLOAT32)f32_ters = ModbusTcpClient.convert_to_registers(x_mm, DT.FLOAT32, word_order="little")print("FLOAT32 big   :", f32, [f"0x{r:04X}" for r in f32])print("FLOAT32 little:", f32_ters, [f"0x{r:04X}" for r in f32_ters]) # Yanlış sırayla okunursa ne görürsünüzprint("ters okunan   :", ModbusTcpClient.convert_from_registers(f32_ters, DT.FLOAT32)) # Aynı koordinat, mikron cinsinden 32 bit tamsayıx_um = round(x_mm * 1000)y_um = round(-87.5 * 1000)i32 = ModbusTcpClient.convert_to_registers([x_um, y_um], DT.INT32)print("INT32 x, y    :", i32)print("geri          :", ModbusTcpClient.convert_from_registers(i32, DT.INT32)) # Çıktı:# FLOAT32 big   : [17142, 59769] ['0x42F6', '0xE979']# FLOAT32 little: [59769, 17142] ['0xE979', '0x42F6']# ters okunan   : -1.8833671448184637e+25# INT32 x, y    : [1, 57920, 65534, 43572]# geri          : [123456, -87500]

123.456 float32'de 0x42F6E979 olur ve varsayılan düzende yüksek word önce gider. Cihaz word'leri ters bekliyorsa aynı dört bayt −1,88 × 10²⁵ olarak okunur. Bu kadar saçma bir sayıyı fark edersiniz, tehlikelisi makul görünen yanlıştır. 5 µm'lik bir değer INT32 olarak [0, 5] register'larına yazılır. Ters word sırasıyla okunduğunda 327.680 µm, yani makinenin hareket alanında kalabilecek 327,68 mm olur.

word_order yalnızca register'ların sırasını değiştirir, register içindeki baytlara dokunmaz, çünkü spesifikasyon orayı big-endian olarak sabitlemiştir. Yine de bazı cihazlar baytları da ters dizer. Dört düzenin hepsini kendiniz üretmek için küçük bir yardımcı yeterli:

python
import struct def float32_regs(deger, sira):    """sira: 'ABCD' (Modbus doğal), 'CDAB', 'BADC', 'DCBA'."""    a, b, c, d = struct.pack(">f", deger)    harita = dict(zip("ABCD", (a, b, c, d)))    ham = bytes(harita[h] for h in sira)    return [int.from_bytes(ham[0:2], "big"), int.from_bytes(ham[2:4], "big")]  for sira in ("ABCD", "CDAB", "BADC", "DCBA"):    regs = float32_regs(x_mm, sira)    yanlis = ModbusTcpClient.convert_from_registers(regs, DT.FLOAT32)    print(f"{sira}: {[f'0x{r:04X}' for r in regs]}  ABCD diye okunursa: {yanlis:.6g}") # Çıktı:# ABCD: ['0x42F6', '0xE979']  ABCD diye okunursa: 123.456# CDAB: ['0xE979', '0x42F6']  ABCD diye okunursa: -1.88337e+25# BADC: ['0xF642', '0x79E9']  ABCD diye okunursa: -9.86112e+32# DCBA: ['0x79E9', '0xF642']  ABCD diye okunursa: 1.5185e+35

Hangi düzenin doğru olduğunu tahmin etmeyin. Kılavuzda yazmıyorsa bilinen bir değeri yazıp cihazın kendi ekranından okuyun. 123,456 iyi bir aday, dört baytı da birbirinden farklı. 1,0 kötü bir aday: iki baytı sıfır olduğu için yanlış düzenlerin üçü de 10⁻³⁸ mertebesinde, sıfır gibi görünen bir sayı verir ve bunu "henüz yazılmadı" sanabilirsiniz.

Float32'nin 24 bitlik mantisi 1000 mm civarında 0,00006 mm'lik adım bırakır, CNC için yeterli. Ben yine de koordinatı mikron cinsinden INT32 göndermeyi tercih ederim. 123.456 float32'ye 123.45600128... olarak girer, tamsayıda yuvarlama sorusu hiç doğmaz. Negatif değerler ikiye tümleyenle taşınır, −87,5 mm [65534, 43572] olur. Son sözü PLC programının beklediği tür söyler.

Adres, atomiklik ve el sıkışma

Spesifikasyon iki numaralandırma tanımlar: veri modelinde her blok 1'den n'e numaralanır, PDU'da adresler 0 ile 65535 arasındadır. Kılavuzda "40100" diye geçen holding register, istekte 99 adresiyle gider. Bir register kaymış yazma X'in yarısını Y'nin içine taşır.

python
from pymodbus.framer import FramerRTU, FramerSocketfrom pymodbus.pdu import DecodePDUfrom pymodbus.pdu.register_message import WriteMultipleRegistersRequest istek = WriteMultipleRegistersRequest(address=99, registers=i32, dev_id=1)print("PDU      :", istek.encode().hex(" "))print("RTU çerçeve:", FramerRTU(DecodePDU(False)).buildFrame(istek).hex(" "))istek.transaction_id = 7print("TCP çerçeve:", FramerSocket(DecodePDU(False)).buildFrame(istek).hex(" ")) # Çıktı:# PDU      : 00 63 00 04 08 00 01 e2 40 ff fe aa 34# RTU çerçeve: 01 10 00 63 00 04 08 00 01 e2 40 ff fe aa 34 1a e0# TCP çerçeve: 00 07 00 00 00 0f 01 10 00 63 00 04 08 00 01 e2 40 ff fe aa 34

PDU, RTU'da da TCP'de de aynı: 10 fonksiyon kodu (Write Multiple Registers), 00 63 başlangıç adresi, 00 04 register sayısı, 08 bayt sayısı ve iki INT32. pymodbus'ın encode() çıktısında fonksiyon kodu yok, çerçevelerde var. RTU başa cihaz adresini, sona CRC'yi ekliyor. TCP başa 7 baytlık bir başlık koyuyor, CRC kullanmıyor.

32 bitlik bir değeri iki ayrı Write Single Register (06) isteğiyle yazmayın. İki istek arasında PLC bir tarama döngüsü tamamlarsa yarısı yeni, yarısı eski bir koordinat okur. X ve Y'yi tek bir Write Multiple Registers (16, yani 0x10) isteğiyle gönderin, spesifikasyon tek istekte 123 register'a kadar izin veriyor. Bu da yetmeyebilir, çünkü cihazın yazılan bloğu kendi döngüsüne ne zaman aktaracağı spesifikasyonda tanımlı değil. Güvenli düzen bir el sıkışma: önce koordinatları yazın, sonra ayrı bir register'daki komut sayacını artırın. PLC sayacın değiştiğini görünce koordinatları kopyalayıp aynı sayıyı onay register'ına yazar. Onayı görmeden yeni hedef göndermezsiniz.

Register'a yazmadan önce koordinatı makinenin hareket sınırlarına göre doğrulayın. Zod ile API doğrulama yazısındaki "sınırda doğrula" fikri burada daha da geçerli. Orada geçersiz istek 400 döner, burada takım tablaya iner.

Sonuç

Makineye ilk gerçek koordinatı göndermeden önce üç testi geçin. Kalibrasyonu RMS'e bakarak değil, kurulumda kullanmadığınız bir noktada milimetre cinsinden ölçerek kabul edin. Homografiyi doğrudan makine koordinatlarına karşı kurun ve beşinci bir noktada doğrulayın. Dört baytı birbirinden farklı bilinen bir değeri register'a yazıp PLC'nin ekranında okuyun. Üçünden biri geçmediyse takım malzemeye inmez.

Sıkça Sorulan Sorular

findChessboardCorners yerine findChessboardCornersSB kullanmalı mıyım?
Yeni bir kurulumda evet. OpenCV belgelerine göre findChessboardCornersSB gürültüye daha dayanıklı ve köşeleri doğrudan piksel altı hassasiyetle döndürüyor, cornerSubPix gerekmiyor. Aynı 14 sentetik görüntüde CALIB_CB_ACCURACY flag'iyle gerçek köşeden ortalama sapması 0,064 piksel çıktı, findChessboardCorners ile cornerSubPix ikilisinin yarısı.
Kamerayı tablaya tam dik monte edersem homografiye gerek kalır mı?
Kalır. Tam diklik ölçülemeyen bir varsayımdır ve yalnızca ölçek ile ötelemeye dayanan bir dönüşüm küçük bir eğikliği kenarda hataya çevirir. Homografi afin dönüşümü özel durum olarak içerir. Fazladan gelen iki parametre, son satırdaki h31 ve h32, eğikliği taşır. Kamera gerçekten dikse bu ikisi sıfıra yakın çıkar ve hiçbir şey kaybetmezsiniz.
Kalibrasyonu ne sıklıkla tekrarlamalıyım?
İç parametreleri lens söküldüğünde, odak ya da diyafram değiştiğinde ve kamera gövdesi değiştiğinde tekrarlayın. Aynı modelden iki gövdede bile cx ve cy farklı çıkar, çünkü sensörün gövdeye montajında tolerans var. Endüstriyel lenslerdeki kilit vidalarını sıkın ki odak halkası titreşimle dönmesin. Homografiyi ise referans işareti kontrolü eşiği aştığında yeniden hesaplayın.
BinaryPayloadBuilder ile yazılmış eski kodu convert_to_registers'a taşırken neye dikkat etmeliyim?
Varsayılanlara. pymodbus 3.8.6'da BinaryPayloadBuilder'ın varsayılan bayt sırası little, word sırası big. Parametresiz bir builder 123.456 için 0xF642, 0x79E9 üretiyor, convert_to_registers ise 0x42F6, 0xE979. Eski kod varsayılanlarla cihazla anlaşıyorsa cihaz register içinde ters bayt bekliyor demektir ve yeni API'nin önüne yazıdaki gibi bir bayt çevirme adımı koymanız gerekir.

Kaynakça

Bu yazıyı paylaş

Mustafa Kürşad Başer
Yazar

Mustafa Kürşad Başer

Kıdemli Yazılım Mühendisi

Karmaşık sorunlara zarif çözümler üretmekten keyif alan, tutkulu bir yazılım mühendisi. Kodlamanın ötesinde, teknoloji, sanat ve insan bilincinin kesişim noktalarını keşfetmekle derinden ilgileniyor.