Mała aktualizacja moich postępów w budowie autofocusa na gumce. Oto obecna konstrukcja. Jako napęd zastosowałem tani silniczek krokowy PF35T-48L4. Dla poprawienia trakcji założyłem mu dwa kółka zamiast jednego i dwie gumki. Silniczek jest napędzany z Adafruit DC & Stepper Motort Hat – pozwala ona na sterowanie dwoma silnikami krokowymi. Zasilanie silnika na razie z czterech paluszków, ale to tylko testy.
Nad oprogramowaniem jeszcze pracuję ale jak na razie działa ono super – jest dość powolne, ale jest w stanie ustawić ostrość tak dokładnie że ręcznie nie jestem w stanie tego powtórzyć. Gumka okazała się bardzo wdzięcznym sposobem napędzania obiektywu i przy odpowiednim sposobie poruszania silnikiem pozwala na bardzo dokładne ustawienie ostrości. Miałem pewne obawy czy ten silniczek, który w końcu ma tylko 48 kroków na obrót będzie wystarczająco dokładny, ale w połączeniu ze sporym przełożeniem dokładność jest raczej wystarczająca. Jednak zamówiłem silniczek 28BYJ-48 – ten ma też 48 kroków ale jednocześnie ma przekładnię 1:64 co daje znacznie większą precyzję.
Wziąłem sobie także do serca ideę zaproponowaną przez jednego z czytelników i po zakończeniu działań z gumką mam zamiar spróbować zbudować AF napędzany paskiem zębatym. Zakupiłem już niezbędny pasek, łożyska, zębatki itp. ale będzie to wymagało znacznie bardziej skomplikowanej konstrukcji mechanicznej niż obecnie. Cóż, poćwiczę sobie obróbkę drewna 😉
https://www.youtube.com/watch?v=Ejaw1uHHKFo
Dla ciekawskich poniżej obecna wersja algorytmu ustawiania ostrości. Python jest dla mnie nowością, więc proszę się nie śmiać z jakichś głupich konstrukcji:
#!/usr/bin/python import atexit import collections import itertools import urllib import time import cv2 from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_StepperMotor import numpy as np resolutionX = 1024 resolutionY = 768 windowX = 160 windowY = 80 maxPosition = 800 fastMinDifference = 50 slowMinDifference = 20 startDirection = Adafruit_MotorHAT.FORWARD memoryLength = 800 fastFocusWindow = 3 slowFocusWindow = 6 verySlowFocusWindow = 200 minSlowStepsBeforeFocus = 5 numOfFastScans = 2 numOfSlowScans = 2 mh = Adafruit_MotorHAT() def turnOffMotors(): mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE) mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE) mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE) mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE) atexit.register(turnOffMotors) myStepper = mh.getStepper(48, 1) myStepper.setSpeed(20) class sliceable_deque(collections.deque): def __getitem__(self, index): if isinstance(index, slice): return type(self)(itertools.islice(self, index.start, index.stop, index.step)) return collections.deque.__getitem__(self, index) def createBuffer(window): return sliceable_deque(maxlen=window) def addSample(sample, holder): holder.append(sample) def rollingMax(holder): return reduce(lambda res, x: res if res > x else x, holder) def rollingMin(holder): return reduce(lambda res, x: res if res < x else x, holder) def rollingAverage(holder): return reduce(lambda res, x: res + x, holder) / len(holder) def rollingSum(holder): return reduce(lambda res, x: res + x, holder) def rollingAllPositive(holder): return reduce(lambda res, x: res if x > 0 else False , holder, True) def rollingAnyPositive(holder): return reduce(lambda res, x: res if res or x > 0 else False , holder, False) def rollingAllNegative(holder): return reduce(lambda res, x: res if x < 0 else False , holder, True) def rollingAnyNegative(holder): return reduce(lambda res, x: res if res or x < 0 else False , holder, False) def rollingAllZero(holder): return reduce(lambda res, x: res if x == 0 else False , holder, True) def rollingDeltas(holder): if len(holder) < 2: return [0] one = holder[:len(holder) - 1] two = holder[1:] zipped = zip(one, two) return [ (b - a) for (a, b) in zipped ] m = np.array([-1, 2, 1], dtype=np.float64) g = cv2.getGaussianKernel(3, -1, cv2.CV_64F) def laplacian(image): return cv2.Laplacian(grayscale, cv2.CV_64F).var() def modifiedLaplacian(image): lx = cv2.sepFilter2D(image, cv2.CV_64F, m, g) ly = cv2.sepFilter2D(image, cv2.CV_64F, g, m) fm = cv2.add(cv2.absdiff(lx, 0) , cv2.absdiff(ly, 0)) return cv2.mean(fm)[0] def tenengrad(image): gx = cv2.Sobel(image, cv2.CV_64F, 1, 0, 3) gy = cv2.Sobel(image, cv2.CV_64F, 0, 1, 3) fm = np.multiply(gx, gx) + np.multiply(gy, gy) return cv2.mean(fm)[0] def normalizedGraylevelVariance(image): mu, sigma = cv2.meanStdDev(image) return (sigma * sigma / mu)[0][0] def canny(image): canny = cv2.Canny(image, 255, 175) nonZero = cv2.countNonZero(canny) return nonZero measure2 = createBuffer(memoryLength) startSliceX = resolutionX / 2 - windowX / 2 startSliceY = resolutionY / 2 - windowY / 2 endSliceX = startSliceX + windowX endSliceY = startSliceY + windowY numberOfDirectionChanges = 0 mode = 0 stepNum = 0 stepsSinceLastSwitch = 0 direction = startDirection if startDirection == Adafruit_MotorHAT.FORWARD: position = 0 myStepper.step(100, Adafruit_MotorHAT.BACKWARD, Adafruit_MotorHAT.SINGLE) myStepper.step(4, Adafruit_MotorHAT.FORWARD, Adafruit_MotorHAT.SINGLE) else: position = maxPosition myStepper.step(100, Adafruit_MotorHAT.FORWARD, Adafruit_MotorHAT.SINGLE) myStepper.step(4, Adafruit_MotorHAT.BACKWARD, Adafruit_MotorHAT.SINGLE) time.sleep(2) def switchDirection(): global direction global numberOfDirectionChanges global stepsSinceLastSwitch if direction == Adafruit_MotorHAT.FORWARD: print "Backward" direction = Adafruit_MotorHAT.BACKWARD else: print "Forward" direction = Adafruit_MotorHAT.FORWARD releaseTension() numberOfDirectionChanges += 1 stepsSinceLastSwitch = 0 def adjustPosition(howMuch): global position if direction == Adafruit_MotorHAT.FORWARD: position += howMuch else: position -= howMuch def releaseTension(): myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) adjustPosition(4*8) def fast(): myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) adjustPosition(8) def slow(): myStepper.oneStep(direction, Adafruit_MotorHAT.MICROSTEP) adjustPosition(1) verySlowCounter = 0 verySlowThreshold = 5 MODE_FAST=1 MODE_SLOW=2 MODE_VERY_SLOW=3 mode = MODE_FAST stream = urllib.urlopen('http://127.0.0.1:8081') jpegData = '' def verySlow(): global verySlowCounter verySlowCounter += 1 if verySlowCounter >= verySlowThreshold: myStepper.oneStep(direction, Adafruit_MotorHAT.MICROSTEP) adjustPosition(1) verySlowCounter = 0 notInFocus = True while (notInFocus): jpegData += stream.read(1024) jpegStart = jpegData.find('\xff\xd8') jpegEnd = jpegData.find('\xff\xd9') if jpegStart != -1 and jpegEnd != -1: jpg = jpegData[jpegStart:jpegEnd + 2] jpegData = jpegData[jpegEnd + 2:] image = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_COLOR) smaller = image[startSliceY:endSliceY, startSliceX:endSliceX, :] grayscale = cv2.cvtColor(smaller, cv2.COLOR_BGR2GRAY) addSample(tenengrad(grayscale), measure2) deltas2 = rollingDeltas(measure2) accel2 = rollingDeltas( deltas2 ) max2 = rollingMax(measure2) print "Focus: {:.0f},{:.0f},{:.0f},{:.0f},{:.0f},{:.0f},{:.0f},{:.0f},{:.0f}".format( measure2[-1], deltas2[-1], accel2[-1], max2, position, mode, direction, fastMinDifference,slowMinDifference) # check if we reached max focus if mode == MODE_VERY_SLOW and stepsSinceLastSwitch >= minSlowStepsBeforeFocus and measure2[-1] >= max2*0.99: notInFocus = False if direction == Adafruit_MotorHAT.FORWARD: direction = Adafruit_MotorHAT.BACKWARD else: direction = Adafruit_MotorHAT.FORWARD myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) myStepper.oneStep(direction, Adafruit_MotorHAT.SINGLE) break if mode == MODE_FAST: if stepsSinceLastSwitch >= 2*fastFocusWindow and rollingSum( deltas2[-(2*fastFocusWindow):-fastFocusWindow] ) > fastMinDifference and rollingSum( deltas2[-fastFocusWindow:] ) < fastMinDifference: switchDirection() else: if numberOfDirectionChanges < numOfFastScans: fast() else: mode = MODE_SLOW elif mode == MODE_SLOW: if stepsSinceLastSwitch >= 2*slowFocusWindow and rollingSum( deltas2[-(2*slowFocusWindow):-slowFocusWindow] ) > slowMinDifference and rollingSum( deltas2[-slowFocusWindow:] ) < slowMinDifference: switchDirection() elif stepsSinceLastSwitch >= min( memoryLength, maxPosition) and rollingAverage( deltas2 ) < 0: slowMinDifference = rollingMax( deltas2 ) switchDirection() else: if numberOfDirectionChanges < numOfSlowScans + numOfFastScans: slow() else: mode = MODE_VERY_SLOW else: if stepsSinceLastSwitch >= verySlowFocusWindow and rollingSum( deltas2[-verySlowFocusWindow:] ) < 0 and rollingSum( deltas2[-verySlowFocusWindow/2:] ) < 0 and rollingSum( deltas2[-verySlowFocusWindow/3:] ) < 0: switchDirection() else: verySlow() stepNum = stepNum + 1 stepsSinceLastSwitch = stepsSinceLastSwitch + 1 if position > maxPosition or position < 0: switchDirection() if mode == MODE_FAST: fastMinDifference = min( fastMinDifference, rollingMax( deltas2 ) ) else: slowMinDifference = min( slowMinDifference, rollingMax( deltas2 ) )