target/
camera.a
-cube.npy
+data/
__pycache__/
cargo.lock
-main:
+main :
+ #
+
+lut : .venv/touchfile
+ .venv/bin/python src/generate_lut.py
+
+distort : .venv/touchfile
+ .venv/bin/python src/generate_distort.py
+
+.venv/touchfile : requirements.txt
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
- .venv/bin/python make.py
+ touch .venv/touchfile
+++ /dev/null
-from .config import IMAGE_WIDTH, IMAGE_HEIGHT, CAP_WAIT
-
-import numpy as np
-import cv2 as cv
-
-
-class Camera():
- def __init__(self, device):
- cv.namedWindow("LUT Calibration", cv.WINDOW_GUI_NORMAL)
-
- self.camera = cv.VideoCapture(device)
- self.homography = None
-
- #self.calibrate()
-
- # get image from camera and fix perspective
- def get(self, image):
- cv.imshow("LUT Calibration", image)
- cv.waitKey(CAP_WAIT)
-
- """
- _, capture = self.camera.read()
- capture = cv.warpPerspective(
- capture,
- self.homography,
- (IMAGE_WIDTH, IMAGE_HEIGHT)
- )
- """
-
- return image
- return capture
-
- # standard calibration function
- def calibrate(self):
- calibration_image = cv.imread("../calibration.jpg")
-
- # remove toolbar from named calibration window
- cv.imshow("LUT Calibration", calibration_image)
- cv.waitKey(0)
-
- _, capture = self.camera.read()
-
- sift = cv.SIFT_create()
- kp1, des1 = sift.detectAndCompute(calibration_image, None)
- kp2, des2 = sift.detectAndCompute(capture, None)
-
- # get good matches between calibration image and the captured image
- flann = cv.FlannBasedMatcher(
- {"algorithm": 1, "trees": 5},
- {"checks": 50}
- )
- matches = flann.knnMatch(des1, des2, k=2)
-
- #get good matches via ratio test
- good = []
- for m,n in matches:
- if m.distance < 0.7 * n.distance:
- good.append(m)
-
- if len(good) > 10:
- src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
- dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
- self.homography, _ = cv.findHomography(dst_pts, src_pts, cv.RANSAC, 5.0)
-
- else:
- raise Exception("Calibration failed")
+++ /dev/null
-"""
-edge length of LUT
-
-very light testing in python outside of the real world has shown 12 to be
-optimal, conventially 33 is used. This will need to be tested with a camera.
-"""
-LUT_SIZE = 12
-
-IMAGE_WIDTH = 640 #1920
-IMAGE_HEIGHT = 360 #1080
-
-# size of the qr codes and the space around them (px)
-QR_SIZE = 100
-QR_PADDING = 10
-
-# the wait time between each camera check after we stop generating
-WAIT_TIME = 0.1 #s
-# number of times to wait
-WAIT_TICKS = 100 #s
-
-# how long to wait before capturing image (1 minimum)
-CAP_WAIT = 1
+++ /dev/null
-from .frame import blank, generate, result
-from .config import LUT_SIZE, WAIT_TIME, WAIT_TICKS
-
-from scipy.interpolate import make_interp_spline
-from tqdm import tqdm
-import numpy as np
-import cv2 as cv
-import time
-
-class Cube():
- def __init__(self, camera):
- self.camera = camera
- self.size = LUT_SIZE
- self.lut = np.zeros((LUT_SIZE, LUT_SIZE, LUT_SIZE, 3), dtype=np.uint8)
-
- # the scaling factor of each color to turn it into a index
- self.s = 255 / (self.size-1)
-
- print(f"""
-creating LUT...
-
-size:\t{self.size}
-scaler:\t{self.s}
-colors:\t{self.size**3}
- """)
-
- # go through each color and put pass it through the screen/camera
- def process(self):
- seq = [i * self.s for i in range(0,self.size)]
- for r in tqdm(seq):
- for g in seq:
- for b in seq:
- self.check(color=[r, g, b])
-
- for _ in range(WAIT_TICKS):
- pass#self.check()
- #time.sleep(WAIT_TIME)
-
- self.invert()
-
- """
- the rust program needs to be able to index easy into the LUT, this means
- getting the color you have, scaling it, and using it as the index. at
- the moment we have a cube that was indexed by its inputs not by its
- outputs. this is easy if you have a cube of 255 width but we dont.
-
- the solution to this is to take each contour of each channel, and
- interpolate it at the regular inteval of the size of the LUT. this means
- that the quality of the interpolation algorithim is the quality of the
- LUT. see below.
- """
- def invert(self):
-
- print("inverting LUT...")
-
- # helper function to rotate and resample each channel
- def iter_channels(a):
- r = self.lut[:,:,:,0]
- g = self.lut[:,:,:,1]
- b = self.lut[:,:,:,2]
-
- r = resample_channel(np.rot90(r, 1, (0,2)))
- g = resample_channel(np.rot90(g, 1, (1,2)))
-
- r = np.rot90(r, -1, (0,2))
- g = np.rot90(g, -1, (1,2))
-
- b = resample_channel(b)
-
- return np.stack((r, g, b), axis=-1)
-
- # helper function to resample a channel
- def resample_channel(c):
- c = np.reshape(c, (self.size * self.size, self.size))
-
- for i in range(self.size * self.size):
- seq = np.linspace(0, 255, self.size)
-
- """
- This is the section that does all the heavy lifting by reinterpolating the curves
- at the right ordinates. scipy b splines should work better but might introduce
- strange loops in weird color situations.
- """
-
- spl = make_interp_spline(c[i], seq)
- c[i] = spl(seq)
-
- # Alternative np splines
-
- #c[i] = np.interp(seq, c[i], seq)
-
- c = np.reshape(c, (self.size, self.size, self.size))
- return c
-
- self.lut = iter_channels(self.lut)
-
- """
- 'check' what a color looks like through the camera. color returned may
- not be the transformed version of the color you put in due to the lag
- between the display and the capture. waiting a long time, clearing the
- cap etc.. all proved ineffective. therefore all images are tagged with a
- qr code of their color.
- """
- def check(self, color=None):
- if color is None:
- image = blank()
- else:
- image = generate(color)
-
- capture = self.camera.get(image)
-
- data, new = result(capture)
-
- if data is not None:
- data = np.divide(data, self.s).round().astype(np.uint8)
-
- self.lut[data[0], data[1], data[2]] = new
-
-
-
+++ /dev/null
-from .config import QR_SIZE, QR_PADDING, IMAGE_WIDTH, IMAGE_HEIGHT
-
-from pyzbar.pyzbar import decode
-import numpy as np
-import cv2 as cv
-import random
-import qrcode
-import os
-
-# helper function to apply non-linear error to each channel of image
-def cast(a):
- a = np.array(a, dtype=np.float64)
- a[...,0] = ((a[...,0] / 255.) ** 2) * 255.
- a[...,1] = ((a[...,1] / 255.) ** 2) * 255.
- a[...,2] = ((a[...,2] / 255.) ** 2) * 255.
- a = np.clip(a.astype(np.uint8), 0, 255)
-
- return a
-
-# generate image display color tagged with qr code
-def generate(color):
- # make qr code
- qr = qrcode.QRCode(
- version=1,
- error_correction=qrcode.constants.ERROR_CORRECT_L,
- border=4,
- )
- qr.add_data(" ".join(["{:03d}".format(int(x)) for x in color]))
- qr.make(fit=True)
-
- # transform qr into array with correct shape
- qr_image = np.array(qr.get_matrix())
- qr_image = np.where(qr_image, 0, 255).astype(np.uint8)
- qr_image = np.repeat(qr_image[:, :, np.newaxis], 3, axis=2)
- qr_image = cv.resize(qr_image, (QR_SIZE,QR_SIZE), interpolation=cv.INTER_NEAREST)
-
- color = cast(color)
-
- # create color image of correct shape
- c_image = np.array([[color[::-1]]], dtype=np.uint8)
- c_image = cv.resize(c_image, (IMAGE_WIDTH, IMAGE_HEIGHT))
-
- # put qr codes in the corners
- tl = np.s_[:QR_SIZE,:QR_SIZE]
- tr = np.s_[:QR_SIZE,-QR_SIZE:]
- bl = np.s_[-QR_SIZE:,:QR_SIZE]
- br = np.s_[-QR_SIZE:,-QR_SIZE:]
-
- c_image[tl] = c_image[tr] = c_image[bl] = c_image[br] = qr_image
-
- return c_image
-
-# make blank image
-def blank():
- image = np.zeros((IMAGE_HEIGHT,IMAGE_WIDTH,3), dtype=np.uint8)
- return image
-
-# decode captured image to origional color and new color
-def result(capture):
- l = QR_SIZE + QR_PADDING
-
- """
- get the mean across a rectangular section of the image described by the
- innermost corners of all the qr codes.
- """
- new = np.mean(capture[l:-l,l:-l], axis=(0,1)).astype(np.uint8)
-
- codes = decode(capture)
-
- if codes == []: return None, None
-
- # always use the code which has the best quality capture
- codes.sort(key=lambda x:x.quality)
- data = codes[0].data
- data = [int(x) for x in data.split()]
- data = np.array(data)
-
- return data, new[::-1]
+++ /dev/null
-#!.venv/bin/python
-
-import matplotlib.pyplot as plt
-import numpy as np
-
-# make dual plot showing the curves and the 3d plot
-def show(a):
- size = a.shape[0]
- fig = plt.figure(figsize=plt.figaspect(1.))
-
- ax = fig.add_subplot(2, 1, 1)
-
- b = a.astype(np.float64)
-
- # get slice of the LUT to graph as the curves
- ax.plot(b[1,1,...,2]+50, c="blue")
- ax.plot(b[1,...,1,1]+25, c="green")
- ax.plot(b[...,1,1,0], c="red")
-
- ax = fig.add_subplot(2, 1, 2, projection='3d')
-
- xs = []
- ys = []
- zs = []
- cs = []
-
- # look im going to do this in a lazy way please forgive me
- for x in range(size):
- for y in range(size):
- for z in range(size):
- xs.append(x)
- ys.append(y)
- zs.append(z)
-
- r, g, b = a[x][y][z]
- cs.append("#{0:02x}{1:02x}{2:02x}".format(r, g, b))
-
- ax.scatter(xs, ys, zs, c=cs)
- ax.set_xlabel("r")
- ax.set_ylabel("g")
- ax.set_zlabel("b")
- plt.show()
-
-# unused
-def compare(a, b):
- plt.hist(a.flat, bins=range(100), fc='k', ec='k', color="red")
- plt.hist(b.flat, bins=range(100), fc='k', ec='k', color="blue")
- plt.show()
-
-if __name__ == "__main__":
- a = np.load("../../cube.npy")
- show(a)
+++ /dev/null
-from .frame import cast
-from .graph import compare
-
-import cv2 as cv
-import numpy as np
-import time
-
-# monolithic validator
-def validate(cube):
- print("testing LUT...")
-
- # get test image and apply cast to it
- image = cv.imread("src/calibration.jpg")
- height, width, _ = image.shape
- a = cast(np.flip(image, axis=-1)).astype(np.uint8)
- casted = cast(np.flip(image, axis=-1)).astype(np.uint8)
-
- # track the application time
- start = time.time()
-
- # scale cube
- a = np.divide(a, cube.s)
-
- # calculate the two points enclosing the real point as well as the remainder
- c1 = np.floor(a).astype(np.uint8)
- c2 = np.ceil(a).astype(np.uint8)
- rem = np.remainder(a, 1)
-
- # helper function to index at location
- def index_lut(a, i):
- for ih in range(height):
- for iw in range(width):
- pos = i[ih,iw]
- pos = np.clip(pos, 0, a.shape[0])
- i[ih,iw] = a[pos[0], pos[1], pos[2]]
-
- return i
-
- # get the real values for each of the enclosing colors
- c1 = index_lut(cube.lut, c1)
- c2 = index_lut(cube.lut, c2)
-
- # average between the two colors weighted with the remainder
- a = c1 + np.array((c2 - c1) * rem, dtype=np.uint8)
- a = np.flip(a, axis=-1)
-
- dur = time.time() - start
-
- # flip to bgr for display
- casted = np.flip(casted, axis=-1)
-
- # do the diff
- diff = np.abs(image.sum(axis=-1, dtype=np.int16) - a.sum(axis=-1, dtype=np.int16))
- diff = np.clip(diff, 0, 255).astype(np.uint8)
- diff = np.stack((diff, diff, diff), axis=-1)
-
- print(f"""
-cast mean:\t\t{np.mean(np.abs(casted - a))}
-
-max error:\t\t{diff.max()}
-mean error:\t\t{np.mean(diff)}
-standard deviation:\t{np.std(diff)}
-
-time taken:\t\t{dur}s
- """)
-
- # make the composite image
- left = np.vstack((image, a), dtype=np.uint8)
- right = np.vstack((casted, diff), dtype=np.uint8)
-
- composite = np.hstack((left, right))
-
- composite = cv.resize(composite, (640,360))
- cv.imshow("LUT Calibration", composite)
-
- cv.waitKey(0)
+++ /dev/null
-from cube.camera import Camera
-from cube.graph import show
-from cube.test import validate
-from cube.cube import Cube
-
-from numpy import save
-import matplotlib.pyplot as plt
-
-if __name__ == "__main__":
- # make camera object
- eye = Camera(0)
-
- # make the LUT and process through the camera
- lut = Cube(eye)
- lut.process()
-
- # use graphing func to show the curves for debug
- show(lut.lut)
-
- # validate error of LUT
- validate(lut)
-
- save("./cube.npy", lut.lut)
-
-
--- /dev/null
+"""
+taken from:
+https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
+
+Camera specific but is simple to use and should only need to be used once per
+camera to get the best possible results. Better pictures is better mapping.
+"""
+
+import numpy as np
+import cv2 as cv
+import glob
+
+# termination criteria
+criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
+
+# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
+objp = np.zeros((6*9,3), np.float32)
+objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2)
+
+# Arrays to store object points and image points from all the images.
+objpoints = [] # 3d point in real world space
+imgpoints = [] # 2d points in image plane.
+
+images = glob.glob("./data/camera/*.jpg")
+
+for fname in images:
+ img = cv.imread(fname)
+ gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
+
+ # Find the chess board corners
+ ret, corners = cv.findChessboardCorners(gray, (9,6), None)
+
+ # If found, add object points, image points (after refining them)
+ if ret == True:
+ objpoints.append(objp)
+
+ corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
+ imgpoints.append(corners2)
+
+ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
+
+img = cv.imread(images[-1])
+h, w = img.shape[:2]
+newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
+
+# undistort
+mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
+dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
+
+full_map = np.stack((mapx, mapy))
+np.save("./data/map.npy", full_map)
+
+# crop the image
+x, y, w, h = roi
+dst = dst[y:y+h, x:x+w]
+
+img = cv.resize(img, (w, h))
+dst = np.concat((dst, img), axis=1)
+
+cv.namedWindow("Result", cv.WINDOW_GUI_NORMAL)
+cv.imshow('Result', dst)
+cv.waitKey(0)
--- /dev/null
+from lut.camera import Camera
+from lut.graph import show
+from lut.test import validate
+from lut.cube import Cube
+
+from numpy import save
+import matplotlib.pyplot as plt
+
+import cv2 as cv
+
+if __name__ == "__main__":
+
+ # make camera object
+ eye = Camera(0)
+
+ # make the LUT and process through the camera
+ lut = Cube(eye)
+ lut.process()
+
+ # use graphing func to show the curves for debug
+ show(lut.lut)
+
+ # validate error of LUT
+ validate(lut)
+
+ save("../data/cube.npy", lut.lut)
+
+
--- /dev/null
+from .config import IMAGE_WIDTH, IMAGE_HEIGHT, CAP_WAIT
+
+import numpy as np
+import cv2 as cv
+
+
+class Camera():
+ def __init__(self, device):
+ cv.namedWindow("LUT Calibration", cv.WINDOW_GUI_NORMAL)
+
+ self.camera = cv.VideoCapture(device)
+ self.homography = None
+
+ #self.calibrate()
+
+ # get image from camera and fix perspective
+ def get(self, image):
+ small = cv.resize(image, (640,360))
+ cv.imshow("LUT Calibration", small)
+ cv.waitKey(CAP_WAIT)
+
+ #_, capture = self.camera.read()
+ capture = image
+
+ if self.homography is not None:
+ capture = cv.warpPerspective(
+ capture,
+ self.homography,
+ (IMAGE_WIDTH, IMAGE_HEIGHT)
+ )
+
+ return image
+ return capture
+
+ # standard calibration function
+ def calibrate(self):
+ calibration_image = cv.imread("../calibration.jpg")
+
+ # remove toolbar from named calibration window
+ cv.imshow("LUT Calibration", calibration_image)
+ cv.waitKey(0)
+
+ _, capture = self.camera.read()
+
+ sift = cv.SIFT_create()
+ kp1, des1 = sift.detectAndCompute(calibration_image, None)
+ kp2, des2 = sift.detectAndCompute(capture, None)
+
+ # get good matches between calibration image and the captured image
+ flann = cv.FlannBasedMatcher(
+ {"algorithm": 1, "trees": 5},
+ {"checks": 50}
+ )
+ matches = flann.knnMatch(des1, des2, k=2)
+
+ #get good matches via ratio test
+ good = []
+ for m,n in matches:
+ if m.distance < 0.7 * n.distance:
+ good.append(m)
+
+ if len(good) > 10:
+ src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
+ dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
+ self.homography, _ = cv.findHomography(dst_pts, src_pts, cv.RANSAC, 5.0)
+
+ else:
+ raise Exception("Calibration failed")
--- /dev/null
+"""
+edge length of LUT
+
+very light testing in python outside of the real world has shown 12 to be
+optimal, conventially 33 is used. This will need to be tested with a camera.
+"""
+LUT_SIZE = 12
+
+IMAGE_WIDTH = 1920 #1920
+IMAGE_HEIGHT = 1080 #1080
+
+# size of the qr codes and the space around them (px)
+QR_SIZE = 300
+QR_PADDING = 10
+
+# the wait time between each camera check after we stop generating
+WAIT_TIME = 0.1 #s
+# number of times to wait
+WAIT_TICKS = 100 #s
+
+# how long to wait before capturing image (1 minimum)
+CAP_WAIT = 1
--- /dev/null
+from .frame import blank, generate, result
+from .config import LUT_SIZE, WAIT_TIME, WAIT_TICKS
+
+from scipy.interpolate import make_interp_spline
+from tqdm import tqdm
+import numpy as np
+import cv2 as cv
+import time
+
+class Cube():
+ def __init__(self, camera):
+ self.camera = camera
+ self.size = LUT_SIZE
+ self.lut = np.zeros((LUT_SIZE, LUT_SIZE, LUT_SIZE, 3), dtype=np.uint8)
+
+ # the scaling factor of each color to turn it into a index
+ self.s = 255 / (self.size-1)
+
+ print(f"""
+creating LUT...
+
+size:\t{self.size}
+scaler:\t{self.s}
+colors:\t{self.size**3}
+ """)
+
+ # go through each color and put pass it through the screen/camera
+ def process(self):
+ seq = [i * self.s for i in range(0,self.size)]
+ for r in tqdm(seq):
+ for g in seq:
+ for b in seq:
+ self.check(color=[r, g, b])
+
+ for _ in range(WAIT_TICKS):
+ pass#self.check()
+ #time.sleep(WAIT_TIME)
+
+ self.invert()
+
+ """
+ the rust program needs to be able to index easy into the LUT, this means
+ getting the color you have, scaling it, and using it as the index. at
+ the moment we have a cube that was indexed by its inputs not by its
+ outputs. this is easy if you have a cube of 255 width but we dont.
+
+ the solution to this is to take each contour of each channel, and
+ interpolate it at the regular inteval of the size of the LUT. this means
+ that the quality of the interpolation algorithim is the quality of the
+ LUT. see below.
+ """
+ def invert(self):
+
+ print("inverting LUT...")
+
+ # helper function to rotate and resample each channel
+ def iter_channels(a):
+ r = self.lut[:,:,:,0]
+ g = self.lut[:,:,:,1]
+ b = self.lut[:,:,:,2]
+
+ r = resample_channel(np.rot90(r, 1, (0,2)))
+ g = resample_channel(np.rot90(g, 1, (1,2)))
+
+ r = np.rot90(r, -1, (0,2))
+ g = np.rot90(g, -1, (1,2))
+
+ b = resample_channel(b)
+
+ return np.stack((r, g, b), axis=-1)
+
+ # helper function to resample a channel
+ def resample_channel(c):
+ c = np.reshape(c, (self.size * self.size, self.size))
+
+ for i in range(self.size * self.size):
+ seq = np.linspace(0, 255, self.size)
+
+ """
+ This is the section that does all the heavy lifting by reinterpolating the curves
+ at the right ordinates. scipy b splines should work better but might introduce
+ strange loops in weird color situations.
+ """
+
+ spl = make_interp_spline(c[i], seq)
+ c[i] = spl(seq)
+
+ # Alternative np splines
+
+ #c[i] = np.interp(seq, c[i], seq)
+
+ c = np.reshape(c, (self.size, self.size, self.size))
+ return c
+
+ self.lut = iter_channels(self.lut)
+
+ """
+ 'check' what a color looks like through the camera. color returned may
+ not be the transformed version of the color you put in due to the lag
+ between the display and the capture. waiting a long time, clearing the
+ cap etc.. all proved ineffective. therefore all images are tagged with a
+ qr code of their color.
+ """
+ def check(self, color=None):
+ if color is None:
+ image = blank()
+ else:
+ image = generate(color)
+
+ capture = self.camera.get(image)
+
+ data, new = result(capture)
+
+ if data is not None:
+ data = np.divide(data, self.s).round().astype(np.uint8)
+
+ self.lut[data[0], data[1], data[2]] = new
+
+
+
--- /dev/null
+from .config import QR_SIZE, QR_PADDING, IMAGE_WIDTH, IMAGE_HEIGHT
+
+from pyzbar.pyzbar import decode
+import numpy as np
+import cv2 as cv
+import random
+import qrcode
+import os
+
+# helper function to apply non-linear error to each channel of image
+def cast(a):
+ a = np.array(a, dtype=np.float64)
+ a[...,0] = ((a[...,0] / 255.) ** 2) * 255.
+ a[...,1] = ((a[...,1] / 255.) ** 2) * 255.
+ a[...,2] = ((a[...,2] / 255.) ** 2) * 255.
+ a = np.clip(a.astype(np.uint8), 0, 255)
+
+ return a
+
+# generate image display color tagged with qr code
+def generate(color):
+ # make qr code
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ border=4,
+ )
+ qr.add_data(" ".join(["{:03d}".format(int(x)) for x in color]))
+ qr.make(fit=True)
+
+ # transform qr into array with correct shape
+ qr_image = np.array(qr.get_matrix())
+ qr_image = np.where(qr_image, 0, 255).astype(np.uint8)
+ qr_image = np.repeat(qr_image[:, :, np.newaxis], 3, axis=2)
+ qr_image = cv.resize(qr_image, (QR_SIZE,QR_SIZE), interpolation=cv.INTER_NEAREST)
+
+ color = cast(color)
+
+ # create color image of correct shape
+ c_image = np.array([[color[::-1]]], dtype=np.uint8)
+ c_image = cv.resize(c_image, (IMAGE_WIDTH, IMAGE_HEIGHT))
+
+ # put qr codes in the corners
+ tl = np.s_[:QR_SIZE,:QR_SIZE]
+ tr = np.s_[:QR_SIZE,-QR_SIZE:]
+ bl = np.s_[-QR_SIZE:,:QR_SIZE]
+ br = np.s_[-QR_SIZE:,-QR_SIZE:]
+
+ c_image[tl] = c_image[tr] = c_image[bl] = c_image[br] = qr_image
+
+ return c_image
+
+# make blank image
+def blank():
+ image = np.zeros((IMAGE_HEIGHT,IMAGE_WIDTH,3), dtype=np.uint8)
+ return image
+
+# decode captured image to origional color and new color
+def result(capture):
+ l = QR_SIZE + QR_PADDING
+
+ """
+ get the mean across a rectangular section of the image described by the
+ innermost corners of all the qr codes.
+ """
+ new = np.mean(capture[l:-l,l:-l], axis=(0,1)).astype(np.uint8)
+
+ codes = decode(capture)
+
+ if codes == []: return None, None
+
+ # always use the code which has the best quality capture
+ codes.sort(key=lambda x:x.quality)
+ data = codes[0].data
+ data = [int(x) for x in data.split()]
+ data = np.array(data)
+
+ return data, new[::-1]
--- /dev/null
+#!.venv/bin/python
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+# make dual plot showing the curves and the 3d plot
+def show(a):
+ size = a.shape[0]
+ fig = plt.figure(figsize=plt.figaspect(1.))
+
+ ax = fig.add_subplot(2, 1, 1)
+
+ b = a.astype(np.float64)
+
+ # get slice of the LUT to graph as the curves
+ ax.plot(b[1,1,...,2]+50, c="blue")
+ ax.plot(b[1,...,1,1]+25, c="green")
+ ax.plot(b[...,1,1,0], c="red")
+
+ ax = fig.add_subplot(2, 1, 2, projection='3d')
+
+ xs = []
+ ys = []
+ zs = []
+ cs = []
+
+ # look im going to do this in a lazy way please forgive me
+ for x in range(size):
+ for y in range(size):
+ for z in range(size):
+ xs.append(x)
+ ys.append(y)
+ zs.append(z)
+
+ r, g, b = a[x][y][z]
+ cs.append("#{0:02x}{1:02x}{2:02x}".format(r, g, b))
+
+ ax.scatter(xs, ys, zs, c=cs)
+ ax.set_xlabel("r")
+ ax.set_ylabel("g")
+ ax.set_zlabel("b")
+ plt.show()
+
+# unused
+def compare(a, b):
+ plt.hist(a.flat, bins=range(100), fc='k', ec='k', color="red")
+ plt.hist(b.flat, bins=range(100), fc='k', ec='k', color="blue")
+ plt.show()
+
+if __name__ == "__main__":
+ a = np.load("../../cube.npy")
+ show(a)
--- /dev/null
+from .frame import cast
+from .graph import compare
+
+import cv2 as cv
+import numpy as np
+import time
+
+# monolithic validator
+def validate(cube):
+ print("testing LUT...")
+
+ # get test image and apply cast to it
+ image = cv.imread("src/calibration.jpg")
+ height, width, _ = image.shape
+ a = cast(np.flip(image, axis=-1)).astype(np.uint8)
+ casted = cast(np.flip(image, axis=-1)).astype(np.uint8)
+
+ # track the application time
+ start = time.time()
+
+ # scale cube
+ a = np.divide(a, cube.s)
+
+ # calculate the two points enclosing the real point as well as the remainder
+ c1 = np.floor(a).astype(np.uint8)
+ c2 = np.ceil(a).astype(np.uint8)
+ rem = np.remainder(a, 1)
+
+ # helper function to index at location
+ def index_lut(a, i):
+ for ih in range(height):
+ for iw in range(width):
+ pos = i[ih,iw]
+ pos = np.clip(pos, 0, a.shape[0])
+ i[ih,iw] = a[pos[0], pos[1], pos[2]]
+
+ return i
+
+ # get the real values for each of the enclosing colors
+ c1 = index_lut(cube.lut, c1)
+ c2 = index_lut(cube.lut, c2)
+
+ # average between the two colors weighted with the remainder
+ a = c1 + np.array((c2 - c1) * rem, dtype=np.uint8)
+ a = np.flip(a, axis=-1)
+
+ dur = time.time() - start
+
+ # flip to bgr for display
+ casted = np.flip(casted, axis=-1)
+
+ # do the diff
+ diff = np.abs(image.sum(axis=-1, dtype=np.int16) - a.sum(axis=-1, dtype=np.int16))
+ diff = np.clip(diff, 0, 255).astype(np.uint8)
+ diff = np.stack((diff, diff, diff), axis=-1)
+
+ print(f"""
+cast mean:\t\t{np.mean(np.abs(casted - a))}
+
+max error:\t\t{diff.max()}
+mean error:\t\t{np.mean(diff)}
+standard deviation:\t{np.std(diff)}
+
+time taken:\t\t{dur}s
+ """)
+
+ # make the composite image
+ left = np.vstack((image, a), dtype=np.uint8)
+ right = np.vstack((casted, diff), dtype=np.uint8)
+
+ composite = np.hstack((left, right))
+
+ composite = cv.resize(composite, (640,360))
+ cv.imshow("LUT Calibration", composite)
+
+ cv.waitKey(0)
#include "opencv4/opencv2/calib3d.hpp"
#include "opencv4/opencv2/imgproc.hpp"
+#ifdef CUDA
+#include "opencv4/opencv2/cudawarping.hpp"
+#endif
+
using namespace cv;
using namespace cv::xfeatures2d;
warpPerspective(capture, capture, homography, capture.size());
resize(capture, buffer, buffer.size());
}
+
+ void ApplyUndistort(uint8_t *camera_ptr, float *xmat_ptr, float *ymat_ptr)
+ {
+ Mat xmat (IMAGE_HEIGHT, IMAGE_WIDTH, CV_32F, xmat_ptr);
+ Mat ymat (IMAGE_HEIGHT, IMAGE_WIDTH, CV_32F, ymat_ptr);
+
+ Mat capture(IMAGE_HEIGHT, IMAGE_WIDTH, CV_8UC3, camera_ptr);
+ Mat buffer = capture.clone();
+
+/* This wont work because the Mats have to be GpuMats, since we're getting a
+ * pointer for them, it might be better to move it all over onto the gpu and
+ * then do the warp transform all at the same time.
+ *
+ * This might be a bit messy.
+ *
+ * Also if im writing CUDA code in cpp and then moving between rust and cpp it
+ * might just be easier to make a lot of the color stuff in rust. Question is is
+ * it more efficient to just do it on the CPU or move it over to the GPU and
+ * then do it there...
+ */
+
+#ifdef CUDA
+ cv::cuda::remap(buffer, capture, xmat, ymat, INTER_NEAREST);
+#else
+ remap(buffer, capture, xmat, ymat, INTER_NEAREST);
+#endif
+ }
}
- possibly see if a version can be made that takes mic input
- implement recording for testing
- implement image display in unused channel
+
+- write cpp code for using cuFFT (not supported by rust-cuda)
+- potentially write rust-cuda kernel for the color conversion