Code:
# tautwire.py
#
# Copyright 2022, John Kasunich
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# DESCRIPTION:
#
# This program uses cheap webcams to look at a taut wire straight
# line reference and calculates where the center of the wire is
# relative to the cameras.
#
# Tested on windows 7 running Python 3.7, might need tweaked for
# a different OS or Python version.
#
# Must have OpenCV-Python installed (this is a one-time operation)
# Type "pip install opencv-python" on the command line to install
#
# It was developed and tested using these cameras:
# https://www.ebay.com/itm/324450174888
# but will probably work with others if you tweak things
#
# I mounted the cameras in this 3D printed housing:
# https://cad.onshape.com/documents/c4...d73439c1815c3c
# but anything that holds the cameras steady and provides
# an evenly illuminated background should work
#
# HOW TO USE:
#
# edit this file as noted below to select cameras, axes, scale, and filename
# look for "EDITME" in the file
#
# run the program and see the camera views on-screen
#
# push the wire lightly in each direction to see which camera responds
# and confirm that the scale factors (including their sign, +/-) make sense
#
# once running you can simply read the wire shift from the screen, or
# hit 'p' to print the current values to the CSV file
# hit 'r' to reset the sample number in the CSV file to zero
# and print a new header line - useful for doing multiple runs
# hit 'q' to quit
# ---------- program starts here ----------------------------
# import the packages we will need
# in this case the key ones are opencv (for image processing)
# and tkinter for display
#import os
#import sys
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
#pip install opencv-python to get this
import cv2 as cv
from collections import namedtuple
# a function to put a window in the foreground
def focus(self):
# make the window open in the foreground
self.lift()
self.attributes('-topmost',True)
self.after_idle(root.attributes,'-topmost',False)
# and active
self.after_idle(root.focus_force)
# datatype for channel info
ChannelDef = namedtuple("ChannelDef", "channel axis scale")
channels = []
# EDITME
# define the camera channels you want to use
# windows seems to assign the channel numbers somewhat arbitrarily,
# it may depend on what order you plugged in the cameras
# edit the "channels.append" lines below as needed for your system
# first item (number) is the camera channel number, will need to trial-and-error it
# second item (string) is the axis ID for display and CSV file
# third item (number) is the scale factor in inches (or mm) per pixel
channels.append(ChannelDef(3, "X", -0.000378))
channels.append(ChannelDef(2, "Y", 0.000355))
# EDITME
# change this filename if you want
# each run appends data to the end of the file
# rename the file (when program is not running) to save for later
# or delete it to start fresh
csvfile = open("camera-data.csv","a")
csvfile.write("program start\n");
# open the camera devices
Camera = namedtuple("Camera","cap index window axis scale")
cameras = []
for chan in channels:
camera = Camera(cv.VideoCapture(chan.channel, cv.CAP_DSHOW), chan.channel, "camera "+str(chan.channel)+":"+chan.axis, chan.axis, chan.scale)
if not camera.cap.isOpened():
print("cannot open camera "+str(chan))
else:
cameras.append(camera);
print("camera "+str(chan)+" opened")
print("fps: "+str(camera.cap.get(cv.CAP_PROP_FPS)))
camera.cap.set(cv.CAP_PROP_FPS,10)
print("fps: "+str(camera.cap.get(cv.CAP_PROP_FPS)))
print("axis: "+camera.axis)
print("scale: "+str(camera.scale))
print ("all cameras opened")
# camera setup complete
values = {}
samplenum = 0; #forces print of header to csv file
# this loop runs until the user hits 'q', processing one
# frame each time
while len(cameras) > 0:
# Capture frame-by-frame
for camera in cameras:
ret, frame = camera.cap.read()
# if frame is read correctly ret is True
if ret:
# Our operations on the frame come here
# split into blue/green/red channels
b,g,r = cv.split(frame)
# these cv.imshow() lines display intermediate results for debugging
# leave them commented out unless you really need them
#cv.imshow("red",r)
#cv.imshow("green",g)
#cv.imshow("blue",b)
# blurring the image a bit allows for sub-pixel resolution
# this uses the green channel since I have green backlights
blur = cv.GaussianBlur(g,(7,7),0)
#cv.imshow("blur",blur)
# apply threshold to make a pure black and white image
ret, bw = cv.threshold(blur,127,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
# cv.imshow("b/w",bw)
sum = 0
count = 0
# compute the center in each horizontal row of pixels
# read opencv docs if you want to understand the altorithm below
for y in range(0,478):
slice = bw[y:y+1, 0:639]
M = cv.moments(slice)
if M["m00"] > 0:
center = int(M["m10"]/M["m00"])
# paint one red pixel at the centerline
# looking at the red line lets you see if it is doing something stupid
frame.itemset((y, center, 0),0)
frame.itemset((y, center, 1),0)
frame.itemset((y, center, 2),255)
sum = sum + center
count = count + 1
if count > 0:
# calculate the overall centerlne
center = sum / count
offset = (center-320.0)*camera.scale
values[camera.axis] = offset
center = round(center,1)
offset = round(offset,4)
# paint the results in the corner of the image
frame = cv.putText(frame, str(center), (20,400),cv.FONT_HERSHEY_SIMPLEX,2,(0,0,255),thickness=4)
frame = cv.putText(frame, str(offset), (20,460),cv.FONT_HERSHEY_SIMPLEX,2,(0,0,255),thickness=4)
# Display the resulting frame
cv.imshow(camera.window, frame)
else:
print("Can't read from camera "+str(camera.index))
break
# the rest of this handles the CSV file
if samplenum <= 0:
#print header line
csvfile.write("Sample")
for axis in values.keys():
csvfile.write(","+axis)
csvfile.write("\n")
print("Sample", end="")
for axis in values.keys():
print(","+axis, end="")
print()
samplenum = 1
keypressed = cv.waitKey(1)
if keypressed == ord('r'):
# reset command
samplenum = 0
if keypressed == ord('p'):
# print command
csvfile.write(str(samplenum))
for value in values.values():
csvfile.write(","+str(value))
csvfile.write("\n")
print(str(samplenum), end="")
for value in values.values():
print(","+str(value), end="")
print()
samplenum = samplenum + 1
if keypressed == ord('q'):
print("shutting down")
break
# When everything done, release the capture
for camera in cameras:
camera.cap.release()
csvfile.write("shutting down\n")
csvfile.close()
cv.destroyAllWindows()
print("done")