From: Max Value Date: Thu, 27 Nov 2025 22:12:04 +0000 (+0000) Subject: Re-arranged project + fixed barreling remaping X-Git-Url: https://git.ozva.co.uk/?a=commitdiff_plain;h=8b82cdb47fe7afe093e3cd8f4279e464a2cfac73;p=rust_fft Re-arranged project + fixed barreling remaping --- diff --git a/.gitignore b/.gitignore index 370aa2a..98caaa6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ target/ camera.a -cube.npy +data/ __pycache__/ cargo.lock diff --git a/Makefile b/Makefile index b9e5885..81a4c79 100644 --- 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 index e69de29..0000000 diff --git a/cube/camera.py b/cube/camera.py deleted file mode 100644 index 1c26b4b..0000000 --- a/cube/camera.py +++ /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 index e992e6a..0000000 --- a/cube/config.py +++ /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 index 48dad07..0000000 --- a/cube/cube.py +++ /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 index 83ab87f..0000000 --- a/cube/frame.py +++ /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 index b505aa8..0000000 --- a/cube/graph.py +++ /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 index bd4a540..0000000 --- a/cube/test.py +++ /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 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 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 index 0000000..208db87 --- /dev/null +++ b/src/generate_distort.py @@ -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 index 0000000..4b54ebe --- /dev/null +++ b/src/generate_lut.py @@ -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 index 0000000..e69de29 diff --git a/src/lut/camera.py b/src/lut/camera.py new file mode 100644 index 0000000..00e96e3 --- /dev/null +++ b/src/lut/camera.py @@ -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 index 0000000..980b562 --- /dev/null +++ b/src/lut/config.py @@ -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 index 0000000..48dad07 --- /dev/null +++ b/src/lut/cube.py @@ -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 index 0000000..83ab87f --- /dev/null +++ b/src/lut/frame.py @@ -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 index 0000000..b505aa8 --- /dev/null +++ b/src/lut/graph.py @@ -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 index 0000000..bd4a540 --- /dev/null +++ b/src/lut/test.py @@ -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) diff --git a/src/perspective.cpp b/src/perspective.cpp index b88b394..449bfbb 100644 --- a/src/perspective.cpp +++ b/src/perspective.cpp @@ -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 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 index 0000000..1f9112c Binary files /dev/null and b/test/pattern.png differ diff --git a/todo b/todo index 510f1f8..d66b84c 100644 --- 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