]> OzVa Git service - rust_fft/commitdiff
Re-arranged project + fixed barreling remaping
authorMax Value <greenwoodw50@gmail.com>
Thu, 27 Nov 2025 22:12:04 +0000 (22:12 +0000)
committerMax Value <greenwoodw50@gmail.com>
Thu, 27 Nov 2025 22:12:04 +0000 (22:12 +0000)
24 files changed:
.gitignore
Makefile
cube/__init__.py [deleted file]
cube/camera.py [deleted file]
cube/config.py [deleted file]
cube/cube.py [deleted file]
cube/frame.py [deleted file]
cube/graph.py [deleted file]
cube/test.py [deleted file]
make.py [deleted file]
src/calibration.jpg [deleted file]
src/generate_distort.py [new file with mode: 0644]
src/generate_lut.py [new file with mode: 0755]
src/lut/__init__.py [new file with mode: 0644]
src/lut/camera.py [new file with mode: 0644]
src/lut/config.py [new file with mode: 0644]
src/lut/cube.py [new file with mode: 0755]
src/lut/frame.py [new file with mode: 0644]
src/lut/graph.py [new file with mode: 0755]
src/lut/test.py [new file with mode: 0755]
src/perspective.cpp
test/calibration.jpg [new file with mode: 0644]
test/pattern.png [new file with mode: 0644]
todo

index 370aa2a0a7f20049f49f19268a306c3279fca4b5..98caaa63d46aa9d5b305f29761c23b9992aae7e3 100644 (file)
@@ -1,5 +1,5 @@
 target/
 camera.a
-cube.npy
+data/
 __pycache__/
 cargo.lock
index b9e58857149bd228298d83d0938625216b8572c7..81a4c792fcd5e7cdf743e10fbb25ec35724e5e4e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,13 @@
-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
diff --git a/cube/__init__.py b/cube/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/cube/camera.py b/cube/camera.py
deleted file mode 100644 (file)
index 1c26b4b..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-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")
diff --git a/cube/config.py b/cube/config.py
deleted file mode 100644 (file)
index e992e6a..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-"""
-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
diff --git a/cube/cube.py b/cube/cube.py
deleted file mode 100755 (executable)
index 48dad07..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-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
-
-
-
diff --git a/cube/frame.py b/cube/frame.py
deleted file mode 100644 (file)
index 83ab87f..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-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]
diff --git a/cube/graph.py b/cube/graph.py
deleted file mode 100755 (executable)
index b505aa8..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-#!.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)
diff --git a/cube/test.py b/cube/test.py
deleted file mode 100755 (executable)
index bd4a540..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-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)
diff --git a/make.py b/make.py
deleted file mode 100755 (executable)
index 531a835..0000000
--- a/make.py
+++ /dev/null
@@ -1,25 +0,0 @@
-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)
-
-
diff --git a/src/calibration.jpg b/src/calibration.jpg
deleted file mode 100644 (file)
index 5dd7357..0000000
Binary files a/src/calibration.jpg and /dev/null differ
diff --git a/src/generate_distort.py b/src/generate_distort.py
new file mode 100644 (file)
index 0000000..208db87
--- /dev/null
@@ -0,0 +1,62 @@
+"""
+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)
diff --git a/src/generate_lut.py b/src/generate_lut.py
new file mode 100755 (executable)
index 0000000..4b54ebe
--- /dev/null
@@ -0,0 +1,28 @@
+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)
+
+
diff --git a/src/lut/__init__.py b/src/lut/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/lut/camera.py b/src/lut/camera.py
new file mode 100644 (file)
index 0000000..00e96e3
--- /dev/null
@@ -0,0 +1,68 @@
+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")
diff --git a/src/lut/config.py b/src/lut/config.py
new file mode 100644 (file)
index 0000000..980b562
--- /dev/null
@@ -0,0 +1,22 @@
+"""
+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
diff --git a/src/lut/cube.py b/src/lut/cube.py
new file mode 100755 (executable)
index 0000000..48dad07
--- /dev/null
@@ -0,0 +1,120 @@
+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
+
+
+
diff --git a/src/lut/frame.py b/src/lut/frame.py
new file mode 100644 (file)
index 0000000..83ab87f
--- /dev/null
@@ -0,0 +1,78 @@
+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]
diff --git a/src/lut/graph.py b/src/lut/graph.py
new file mode 100755 (executable)
index 0000000..b505aa8
--- /dev/null
@@ -0,0 +1,52 @@
+#!.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)
diff --git a/src/lut/test.py b/src/lut/test.py
new file mode 100755 (executable)
index 0000000..bd4a540
--- /dev/null
@@ -0,0 +1,76 @@
+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)
index b88b394d904672bffacd2eb8111bd6f2eddbe188..449bfbb53ee609c4bf0c724fddb9978b4ddad60a 100644 (file)
@@ -6,6 +6,10 @@
 #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;
 
@@ -86,5 +90,32 @@ extern "C"
                        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
+               }
 }
 
diff --git a/test/calibration.jpg b/test/calibration.jpg
new file mode 100644 (file)
index 0000000..5dd7357
Binary files /dev/null and b/test/calibration.jpg differ
diff --git a/test/pattern.png b/test/pattern.png
new file mode 100644 (file)
index 0000000..1f9112c
Binary files /dev/null and b/test/pattern.png differ
diff --git a/todo b/todo
index 510f1f8f33538c79c15e9b0fcc89392fdfa8795c..d66b84c90623a385be57ec7bb8372ac100c438da 100644 (file)
--- a/todo
+++ b/todo
@@ -10,3 +10,6 @@ todo:
 - 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