From 6a010b54398e060a76438292ea93d2a29124c229 Mon Sep 17 00:00:00 2001 From: Alex Liberzon Date: Thu, 20 Jun 2024 13:56:29 +0300 Subject: [PATCH 1/5] added scipy.optimize.minimize option. fails on plot --- pyptv/calibration_gui.py | 94 ++++++- pyptv/optimize_calibration.ipynb | 445 +++++++++++++++++++++++++++++++ pyptv/ptv.py | 6 +- 3 files changed, 542 insertions(+), 3 deletions(-) create mode 100644 pyptv/optimize_calibration.ipynb diff --git a/pyptv/calibration_gui.py b/pyptv/calibration_gui.py index 9227d32..124b9ce 100644 --- a/pyptv/calibration_gui.py +++ b/pyptv/calibration_gui.py @@ -977,8 +977,37 @@ def _button_fine_orient_fired(self): flags, ) except BaseException as exc: - raise ValueError("full calibration failed\n") from exc - # save the results + from scipy.optimize import minimize + cal = self.cals[i_cam] + x0 = np.concatenate([ + cal.get_pos(), + cal.get_angles(), + cal.get_primary_point(), + cal.get_radial_distortion(), + cal.get_decentering(), + cal.get_affine(), + ]) + print(x0) + sol = minimize(self._error_function, + x0, + args=(cal, + all_known, + all_detected, + self.cpar + ), + method='Nelder-Mead', + tol=1e-4, + ) + print(f"Difference: {sol.x - x0}") + self._array_to_calibration(sol.x, self.cals[i_cam]) + self._project_cal_points(i_cam) + + # we need to generate self.cals[i_cam] and residuals + # and translate targets to targ_ix + # raise ValueError("full calibration failed\n") from exc + print("\n Calibration using scipy.optimize.minimize \n") + + # save the results from self.cals[i_cam] self._write_ori(i_cam, addpar_flag=True) # Plot the output @@ -1012,6 +1041,65 @@ def _button_fine_orient_fired(self): self.status_text = "Orientation finished." +# Insert parts for scipy.optimize calibration +# + def _array_to_calibration(self, x:np.ndarray, cal:Calibration) -> None: + cal.set_pos(x[:3]) + cal.set_angles(x[3:6]) + cal.set_primary_point(x[6:9]) + # cal.set_radial_distortion(x[9:12]) + # cal.set_decentering(x[12:14]) + # cal.set_affine_distortion(x[14:]) + return None + + def _calibration_to_array(self, cal:Calibration) -> np.ndarray: + print(cal.get_pos()) + print(cal.get_angles()) + print(cal.get_primary_point()) + print(cal.get_radial_distortion()) + print(cal.get_decentering()) + print(cal.get_affine()) + + return np.concatenate([ + cal.get_pos(), + cal.get_angles(), + cal.get_primary_point(), + cal.get_radial_distortion(), + cal.get_decentering(), + cal.get_affine(), + ]) + + def _error_function(self, x, cal, XYZ, xy, cpar): + """Error function for scipy.optimize.minimize. + + Args: + x (array-like): Array of parameters. + cal (Calibration): Calibration object. + XYZ (array-like): 3D coordinates. + xy (array-like): 2D image coordinates. + cpar (CPar): Camera parameters. + + Returns: + float: Error value. + """ + cal.set_pos(x[:3]) + cal.set_angles(x[3:6]) + cal.set_primary_point(x[6:9]) + # cal.set_radial_distortion(x[9:12]) + # cal.set_decentering(x[12:14]) + # cal.set_affine_distortion(x[14:]) + + targets = convert_arr_metric_to_pixel( + image_coordinates(XYZ, cal, cpar.get_multimedia_params()), + cpar + ) + + err = np.sum((xy[:,1:] - targets)**2) + # err = np.sum(xy - targets) + # print(err) + return err + + def _write_ori(self, i_cam, addpar_flag=False): """Writes ORI and ADDPAR files for a single calibration result of i_cam @@ -1174,6 +1262,7 @@ def _read_cal_points(self): with open(self.calParams.fixp_name, 'r') as file: first_line = file.readline() + print(first_line) if ',' in first_line: delimiter=',' elif '\t' in first_line: @@ -1198,6 +1287,7 @@ def _read_cal_points(self): if len(sys.argv) == 1: active_path = Path("../test_cavity/parametersRun1") + active_path = Path("/home/user/Downloads/rbc300/parametersMultiPlain") else: active_path = Path(sys.argv[0]) diff --git a/pyptv/optimize_calibration.ipynb b/pyptv/optimize_calibration.ipynb new file mode 100644 index 0000000..a1efda9 --- /dev/null +++ b/pyptv/optimize_calibration.ipynb @@ -0,0 +1,445 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "# Jupyter notebook version of calibration" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "# from Yosef Meller's PBI\n", + "\n", + "import shutil\n", + "import numpy as np\n", + "import numpy.random as rnd\n", + "from optv.calibration import Calibration\n", + "from optv.imgcoord import image_coordinates\n", + "from optv.transforms import convert_arr_metric_to_pixel\n", + "from optv.segmentation import target_recognition\n", + "from optv.imgcoord import image_coordinates\n", + "from optv.transforms import convert_arr_metric_to_pixel\n", + "from optv.orientation import match_detection_to_ref\n", + "from optv.orientation import external_calibration, full_calibration\n", + "from optv.calibration import Calibration\n", + "from optv.tracking_framebuf import TargetArray\n", + "\n", + "\n", + "from pyptv import ptv, parameter_gui, parameters as par" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "par_path = Path(\"/home/user/Downloads/rbc300/parametersMultiPlain\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "def get_pos(inters, R, angs):\n", + " # Transpose of http://planning.cs.uiuc.edu/node102.html\n", + " # Also consider the angles are reversed when moving from camera frame to\n", + " # global frame.\n", + " s = np.sin(angs)\n", + " c = np.cos(angs)\n", + " pos = inters + R*np.r_[ s[1], -c[1]*s[0], c[1]*c[0] ]\n", + " return pos\n", + "\n", + "def get_polar_rep(pos, angs):\n", + " \"\"\"\n", + " Returns the point of intersection with zero Z plane, and distance from it.\n", + " \"\"\"\n", + " s = np.sin(angs)\n", + " c = np.cos(angs)\n", + " zdir = -np.r_[ s[1], -c[1]*s[0], c[1]*c[0] ]\n", + " \n", + " c = -pos[2]/zdir[2]\n", + " inters = pos + c*zdir\n", + " R = np.linalg.norm(inters - pos)\n", + " \n", + " return inters[:2], R\n", + " \n", + "def gen_calib(inters, R, angs, glass_vec, prim_point, radial_dist, decent):\n", + " pos = get_pos(inters, R, angs)\n", + " return Calibration(pos, angs, prim_point, radial_dist, decent, \n", + " np.r_[1, 0], glass_vec)\n", + "\n", + "def fitness(solution, calib_targs, calib_detect, glass_vec, cpar):\n", + " \"\"\"\n", + " Checks the fitness of an evolutionary solution of calibration values to \n", + " target points. Fitness is the sum of squares of the distance from each \n", + " guessed point to the closest neighbor.\n", + " \n", + " Arguments:\n", + " solution - array, concatenated: position of intersection with Z=0 plane; 3 \n", + " angles of exterior calibration; primary point (xh,yh,cc); 3 radial\n", + " distortion parameters; 2 decentering parameters.\n", + " calib_targs - a (p,3) array of p known points on the calibration target.\n", + " calib_detect - a (d,2) array of d detected points in the calibration \n", + " target.\n", + " cpar - a ControlParams object with image data.\n", + " \"\"\"\n", + " # Breakdown of of agregate solution vector:\n", + " inters = np.zeros(3)\n", + " inters[:2] = solution[:2]\n", + " R = solution[2]\n", + " angs = solution[3:6] \n", + " prim_point = solution[6:9]\n", + " rad_dist = solution[9:12]\n", + " decent = solution[12:14]\n", + " \n", + " # Compare known points' projections to detections:\n", + " cal = gen_calib(inters, R, angs, glass_vec, prim_point, rad_dist, decent)\n", + " known_proj = image_coordinates(calib_targs, cal, \n", + " cpar.get_multimedia_params())\n", + " known_2d = convert_arr_metric_to_pixel(known_proj, cpar)\n", + " dists = np.linalg.norm(\n", + " known_2d[None,:,:] - calib_detect[:,None,:], axis=2).min(axis=0)\n", + " \n", + " return np.sum(dists**2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import os\n", + "working_folder = Path(\"/home/user/Downloads/rbc300\")\n", + "working_folder.exists()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/home/user/Downloads/rbc300\n" + ] + } + ], + "source": [ + "os.chdir(working_folder)\n", + "print(os.getcwd())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "calOriParams = par.CalOriParams(path = working_folder / \"parameters\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "def g(f):\n", + " \"\"\" Returns a line without white spaces \"\"\"\n", + " return f.readline().strip()\n", + "\n", + "def read(filepath, n_img = 4):\n", + " with open(filepath, \"r\") as f:\n", + "\n", + " fixp_name = Path(g(f))\n", + " fixp_name.exists()\n", + "\n", + " img_cal_name = []\n", + " img_ori = []\n", + " for i in range(n_img):\n", + " img_cal_name.append(g(f))\n", + " img_ori.append(g(f))\n", + "\n", + " tiff_flag = int(g(f)) != 0 # <-- overwrites the above\n", + " pair_flag = int(g(f)) != 0\n", + " chfield = int(g(f))\n", + "\n", + "\n", + " # test if files are present, issue warnings\n", + " for i in range(n_img):\n", + " Path(img_cal_name[i]).exists()\n", + " Path(img_ori[i]).exists()\n", + "\n", + " return fixp_name, img_cal_name, img_ori, tiff_flag, pair_flag, chfield\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(['calibration_images/c0/calib_c0.tif',\n", + " 'calibration_images/c1/calib_c1.tif',\n", + " 'calibration_images/c2/calib_c2.tif',\n", + " 'calibration_images/c3/calib_c3.tif'],\n", + " ['cal/cam1.tif.ori',\n", + " 'cal/cam2.tif.ori',\n", + " 'cal/cam3.tif.ori',\n", + " 'cal/cam4.tif.ori'])" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fixp_name, img_cal_name, img_ori, tiff_flag, pair_flag, chfield = read(working_folder / \"parametersMultiPlane\" / \"cal_ori.par\")\n", + "img_cal_name, img_ori" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[PosixPath('/home/user/Downloads/rbc300/parameters/multi_planes.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/criteria.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/track.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/detect_plate.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/sequence.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/shaking.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/man_ori.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/orient.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/targ_rec.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/examine.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/cal_ori.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/unsharp_mask.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/ptv.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/pft_version.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/dumbbell.par'),\n", + " PosixPath('/home/user/Downloads/rbc300/parameters/sortgrid.par')]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(calOriParams.path.rglob('*.par'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# for f in calOriParams.img_ori[:4]:\n", + "# print(f\"Backing up {f}\")\n", + "# shutil.copyfile(f, f + \".bck\")\n", + "# g = f.replace(\"ori\", \"addpar\")\n", + "# shutil.copyfile(g, g + \".bck\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "op = par.OrientParams()\n", + "op.read()\n", + "\n", + "# recognized names for the flags:\n", + "names = [\n", + " \"cc\",\n", + " \"xh\",\n", + " \"yh\",\n", + " \"k1\",\n", + " \"k2\",\n", + " \"k3\",\n", + " \"p1\",\n", + " \"p2\",\n", + " \"scale\",\n", + " \"shear\",\n", + "]\n", + "op_names = [\n", + " op.cc,\n", + " op.xh,\n", + " op.yh,\n", + " op.k1,\n", + " op.k2,\n", + " op.k3,\n", + " op.p1,\n", + " op.p2,\n", + " op.scale,\n", + " op.shear,\n", + "]\n", + "\n", + "flags = []\n", + "for name, op_name in zip(names, op_names):\n", + " if op_name == 1:\n", + " flags.append(name)\n", + "\n", + "for i_cam in range(self.n_cams): # iterate over all cameras\n", + "\n", + " if self.epar.Combine_Flag:\n", + "\n", + " self.status_text = \"Multiplane calibration.\"\n", + " \"\"\" Performs multiplane calibration, in which for all cameras the\n", + " pre-processed planes in multi_plane.par combined.\n", + " Overwrites the ori and addpar files of the cameras specified\n", + " in cal_ori.par of the multiplane parameter folder\n", + " \"\"\"\n", + "\n", + " all_known = []\n", + " all_detected = []\n", + "\n", + " for i in range(\n", + " self.MultiParams.n_planes\n", + " ): # combine all single planes\n", + "\n", + " # c = self.calParams.img_ori[i_cam][-9] # Get camera id\n", + " # not all ends with a number\n", + " c = re.findall(\"\\\\d+\", self.calParams.img_ori[i_cam])[0]\n", + "\n", + " file_known = (\n", + " self.MultiParams.plane_name[i] + c + \".tif.fix\"\n", + " )\n", + " file_detected = (\n", + " self.MultiParams.plane_name[i] + c + \".tif.crd\"\n", + " )\n", + "\n", + " # Load calibration point information from plane i\n", + " try:\n", + " known = np.loadtxt(file_known)\n", + " detected = np.loadtxt(file_detected)\n", + " except BaseException:\n", + " raise IOError(\n", + " \"reading {} or {} failed\".format(\n", + " file_known, file_detected\n", + " )\n", + " )\n", + "\n", + " if np.any(detected == -999):\n", + " raise ValueError(\n", + " (\n", + " \"Using undetected points in {} will cause \"\n", + " + \"silliness. Quitting.\"\n", + " ).format(file_detected)\n", + " )\n", + "\n", + " num_known = len(known)\n", + " num_detect = len(detected)\n", + "\n", + " if num_known != num_detect:\n", + " raise ValueError(\n", + " f\"Number of detected points {num_known} does not match\"\n", + " \" number of known points {num_detect} for \\\n", + " {file_known}, {file_detected}\")\n", + "\n", + " if len(all_known) > 0:\n", + " detected[:, 0] = (\n", + " all_detected[-1][-1, 0]\n", + " + 1\n", + " + np.arange(len(detected))\n", + " )\n", + "\n", + " # Append to list of total known and detected points\n", + " all_known.append(known)\n", + " all_detected.append(detected)\n", + "\n", + " # Make into the format needed for full_calibration.\n", + " all_known = np.vstack(all_known)[:, 1:]\n", + " all_detected = np.vstack(all_detected)\n", + "\n", + " # this is the main difference in the multiplane mode\n", + " # that we fill the targs and cal_points by the\n", + " # combined information\n", + "\n", + " targs = TargetArray(len(all_detected))\n", + " for tix in range(len(all_detected)):\n", + " targ = targs[tix]\n", + " det = all_detected[tix]\n", + "\n", + " targ.set_pnr(tix)\n", + " targ.set_pos(det[1:])\n", + "\n", + " self.cal_points = np.empty((all_known.shape[0],)).astype(\n", + " dtype=[(\"id\", \"i4\"), (\"pos\", \"3f8\")]\n", + " )\n", + " self.cal_points[\"pos\"] = all_known\n", + " else:\n", + " targs = self.sorted_targs[i_cam]\n", + "\n", + "\n", + " residuals, targ_ix, err_est = full_calibration(\n", + " self.cals[i_cam],\n", + " self.cal_points[\"pos\"],\n", + " targs,\n", + " self.cpar,\n", + " flags,\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pyptv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyptv/ptv.py b/pyptv/ptv.py index bf29525..649d3d0 100644 --- a/pyptv/ptv.py +++ b/pyptv/ptv.py @@ -3,6 +3,7 @@ from optv.calibration import Calibration from optv.correspondences import correspondences, MatchedCoords from optv.image_processing import preprocess_image +from optv.imgcoord import image_coordinates from optv.orientation import ( point_positions, external_calibration, @@ -14,11 +15,14 @@ TrackingParams, SequenceParams, TargetParams, + MultimediaParams, ) from optv.segmentation import target_recognition from optv.tracking_framebuf import CORRES_NONE, read_targets, TargetArray from optv.tracker import Tracker, default_naming from optv.epipolar import epipolar_curve +from optv.transforms import convert_arr_metric_to_pixel + from skimage.io import imread from pyptv import parameters as par @@ -539,4 +543,4 @@ def py_multiplanecalibration(exp): # Save the results exp._write_ori(i_cam, addpar_flag=True) # addpar_flag to save addpar file - print("End multiplane") + print("End multiplane") \ No newline at end of file From 70a558704210038fde6fd9ac2d8085c9b150f9ce Mon Sep 17 00:00:00 2001 From: Alex Liberzon Date: Thu, 20 Jun 2024 14:36:57 +0300 Subject: [PATCH 2/5] added editing addpar files --- pyptv/calibration_gui.py | 74 +++++++++++++++++++++++++++++++------ pyptv/code_editor.py | 79 +++++++++++++++++++++++++++++++--------- pyptv/parameters.py | 5 ++- 3 files changed, 127 insertions(+), 31 deletions(-) diff --git a/pyptv/calibration_gui.py b/pyptv/calibration_gui.py index 124b9ce..c3ddc3a 100644 --- a/pyptv/calibration_gui.py +++ b/pyptv/calibration_gui.py @@ -32,7 +32,7 @@ # from chaco.tools.simple_zoom import SimpleZoom from pyptv.text_box_overlay import TextBoxOverlay -from pyptv.code_editor import codeEditor +from pyptv.code_editor import oriEditor, addparEditor from pyptv.quiverplot import QuiverPlot @@ -340,6 +340,7 @@ class CalibrationGUI(HasTraits): button_checkpoint = Button() button_ap_figures = Button() button_edit_ori_files = Button() + button_edit_addpar_files = Button() button_test = Button() # --------------------------------------------------- @@ -480,6 +481,11 @@ def __init__(self, active_path: Path): label="Edit ori files", show_label=False, ), + Item( + name="button_edit_addpar_files", + label="Edit addpar files", + show_label=False, + ), show_left=False, ), ), @@ -1002,19 +1008,28 @@ def _button_fine_orient_fired(self): self._array_to_calibration(sol.x, self.cals[i_cam]) self._project_cal_points(i_cam) - # we need to generate self.cals[i_cam] and residuals - # and translate targets to targ_ix - # raise ValueError("full calibration failed\n") from exc + + residuals = self._residuals(sol.x, + self.cals[i_cam], + all_known, + all_detected, + self.cpar + ) + print("\n Calibration using scipy.optimize.minimize \n") # save the results from self.cals[i_cam] self._write_ori(i_cam, addpar_flag=True) - # Plot the output - # self.reset_plots() + # x, y = [], [] + # for r, t in zip(residuals, targ_ix): + # if t != -999: + # pos = targs[t].pos() + # x.append(pos[0]) + # y.append(pos[1]) x, y = [], [] - for r, t in zip(residuals, targ_ix): + for t in targ_ix: if t != -999: pos = targs[t].pos() x.append(pos[0]) @@ -1072,6 +1087,38 @@ def _calibration_to_array(self, cal:Calibration) -> np.ndarray: def _error_function(self, x, cal, XYZ, xy, cpar): """Error function for scipy.optimize.minimize. + Args: + x (array-like): Array of parameters. + cal (Calibration): Calibration object. + XYZ (array-like): 3D coordinates. + xy (array-like): 2D image coordinates. + cpar (CPar): Camera parameters. + + Returns: + float: Error value. + """ + # cal.set_pos(x[:3]) + # cal.set_angles(x[3:6]) + # cal.set_primary_point(x[6:9]) + # # cal.set_radial_distortion(x[9:12]) + # # cal.set_decentering(x[12:14]) + # # cal.set_affine_distortion(x[14:]) + + # targets = convert_arr_metric_to_pixel( + # image_coordinates(XYZ, cal, cpar.get_multimedia_params()), + # cpar + # ) + + # err = np.sum((xy[:,1:] - targets)**2) + + residuals = self._residuals(x, cal, XYZ, xy, cpar) + err = np.sum(residuals**2) + + return err + + def _residuals(self, x, cal, XYZ, xy, cpar): + """Error function for scipy.optimize.minimize. + Args: x (array-like): Array of parameters. cal (Calibration): Calibration object. @@ -1094,10 +1141,8 @@ def _error_function(self, x, cal, XYZ, xy, cpar): cpar ) - err = np.sum((xy[:,1:] - targets)**2) - # err = np.sum(xy - targets) - # print(err) - return err + residuals = xy[:,1:] - targets + return residuals def _write_ori(self, i_cam, addpar_flag=False): @@ -1204,9 +1249,14 @@ def reset_show_images(self): cam._plot.request_redraw() def _button_edit_ori_files_fired(self): - editor = codeEditor(path=self.par_path) + editor = oriEditor(path=self.par_path) editor.edit_traits(kind="livemodal") + def _button_edit_addpar_files_fired(self): + editor = addparEditor(path=self.par_path) + editor.edit_traits(kind="livemodal") + + def drawcross(self, str_x, str_y, x, y, color1, size1, i_cam=None): """ diff --git a/pyptv/code_editor.py b/pyptv/code_editor.py index 91f09b7..1ca8c35 100644 --- a/pyptv/code_editor.py +++ b/pyptv/code_editor.py @@ -28,41 +28,43 @@ def get_path(filename): ) -def get_code(path): - f = open(path, "r") +def get_code(path: Path): + + print(f"{path} exists: {path.exists()}") + with open(path, "r", encoding="utf-8") as f: + retCode = f.read() + + print(retCode) - retCode = f.read() - f.close() return retCode -class oriEditor(HasTraits): - file_Path = File - ori_Code = Code() - ori_Save = Button(label="Save") +class codeEditor(HasTraits): + file_Path = Path + _Code = Code() + _Save = Button(label="Save") buttons_group = Group( Item(name="file_Path", style="simple", show_label=False, width=0.3), - Item(name="ori_Save", show_label=False), + Item(name="_Save", show_label=False), orientation="horizontal", ) traits_view = View( Group( - Item(name="ori_Code", show_label=False, height=300, width=650), + Item(name="_Code", show_label=False, height=300, width=650), buttons_group, ) ) - def _ori_Save_fired(self, filename, code): + def _Save_fired(self): f = open(self.file_Path, "w") - f.write(self.ori_Code) + f.write(self._Code) f.close() - def __init__(self, file_path): + def __init__(self, file_path: Path): self.file_Path = file_path - self.ori_Code = get_code(file_path) - + self._Code = get_code(file_path) -class codeEditor(HasTraits): +class oriEditor(HasTraits): # number of images n_img = Int() @@ -86,6 +88,47 @@ class codeEditor(HasTraits): title="Camera's orientation files", ) + def __init__(self, path: Path): + """ Initialize by reading parameters and filling the editor windows """ + # load ptv_par + ptvParams = par.PtvParams(path=path) + ptvParams.read() + self.n_img = ptvParams.n_img + + # load cal_ori + calOriParams = par.CalOriParams(self.n_img) + calOriParams.read() + + for i in range(self.n_img): + self.oriEditors.append( + codeEditor(Path(calOriParams.img_ori[i])) + ) + + +class addparEditor(HasTraits): + + # number of images + n_img = Int() + + addparEditors = List + + # view + traits_view = View( + Item( + "addparEditors", + style="custom", + editor=ListEditor( + use_notebook=True, + deletable=False, + dock_style="tab", + page_name=".file_Path", + ), + show_label=False, + ), + buttons=["Cancel"], + title="Camera's additional parameters files", + ) + def __init__(self, path): """ Initialize by reading parameters and filling the editor windows """ # load ptv_par @@ -98,6 +141,6 @@ def __init__(self, path): calOriParams.read() for i in range(self.n_img): - self.oriEditors.append( - oriEditor(calOriParams.img_ori[i]) + self.addparEditors.append( + codeEditor(Path(calOriParams.img_ori[i].replace('ori', 'addpar'))) ) diff --git a/pyptv/parameters.py b/pyptv/parameters.py index b8ca3bb..590e62b 100644 --- a/pyptv/parameters.py +++ b/pyptv/parameters.py @@ -23,10 +23,13 @@ def g(f): class Parameters(HasTraits): # default path of the directory of the param files - default_path = Path("parameters") + default_path = Path(par_dir_prefix) def __init__(self, path: Path=default_path): HasTraits.__init__(self) + if isinstance(path, str): + path = Path(path) + self.path = path.resolve() self.exp_path = self.path.parent From e88b144449e90a4772e392c6c116192313d75aee Mon Sep 17 00:00:00 2001 From: Alex Liberzon Date: Thu, 20 Jun 2024 15:41:17 +0300 Subject: [PATCH 3/5] fixed saving addpar and ori files --- pyptv/calibration_gui.py | 13 ++++++++++--- pyptv/code_editor.py | 22 +++++++++++++--------- pyptv/pyptv_gui.py | 4 +++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pyptv/calibration_gui.py b/pyptv/calibration_gui.py index c3ddc3a..779bd8f 100644 --- a/pyptv/calibration_gui.py +++ b/pyptv/calibration_gui.py @@ -983,8 +983,12 @@ def _button_fine_orient_fired(self): flags, ) except BaseException as exc: + print(f"Error in full_calibration: {exc} go to scipy") + from scipy.optimize import minimize cal = self.cals[i_cam] + print("Before: \n") + x0 = np.concatenate([ cal.get_pos(), cal.get_angles(), @@ -1005,6 +1009,8 @@ def _button_fine_orient_fired(self): tol=1e-4, ) print(f"Difference: {sol.x - x0}") + print("Before: \n") + print(self.cals[i_cam]) self._array_to_calibration(sol.x, self.cals[i_cam]) self._project_cal_points(i_cam) @@ -1017,6 +1023,7 @@ def _button_fine_orient_fired(self): ) print("\n Calibration using scipy.optimize.minimize \n") + targ_ix = np.arange(len(all_detected)) # save the results from self.cals[i_cam] self._write_ori(i_cam, addpar_flag=True) @@ -1062,9 +1069,9 @@ def _array_to_calibration(self, x:np.ndarray, cal:Calibration) -> None: cal.set_pos(x[:3]) cal.set_angles(x[3:6]) cal.set_primary_point(x[6:9]) - # cal.set_radial_distortion(x[9:12]) - # cal.set_decentering(x[12:14]) - # cal.set_affine_distortion(x[14:]) + cal.set_radial_distortion(x[9:12]) + cal.set_decentering(x[12:14]) + cal. set_affine_trans(x[14:]) return None def _calibration_to_array(self, cal:Calibration) -> np.ndarray: diff --git a/pyptv/code_editor.py b/pyptv/code_editor.py index 1ca8c35..38bc1dc 100644 --- a/pyptv/code_editor.py +++ b/pyptv/code_editor.py @@ -29,8 +29,9 @@ def get_path(filename): def get_code(path: Path): + """ Read the code from the file """ - print(f"{path} exists: {path.exists()}") + print(f"Read from {path}: {path.exists()}") with open(path, "r", encoding="utf-8") as f: retCode = f.read() @@ -42,10 +43,10 @@ def get_code(path: Path): class codeEditor(HasTraits): file_Path = Path _Code = Code() - _Save = Button(label="Save") + save_button = Button(label="Save") buttons_group = Group( - Item(name="file_Path", style="simple", show_label=False, width=0.3), - Item(name="_Save", show_label=False), + Item(name="file_Path", style="simple", show_label=True, width=0.3), + Item(name="save_button", show_label=True), orientation="horizontal", ) traits_view = View( @@ -55,11 +56,14 @@ class codeEditor(HasTraits): ) ) - def _Save_fired(self): - f = open(self.file_Path, "w") - f.write(self._Code) - f.close() - + def _save_button_fired(self): + with open(self.file_Path, "w", encoding="utf-8") as f: + print(f"Saving to {self.file_Path}") + print(f"Code: {self._Code}") + f.write(self._Code) + + print(f"Saved to {self.file_Path}") + def __init__(self, file_path: Path): self.file_Path = file_path self._Code = get_code(file_path) diff --git a/pyptv/pyptv_gui.py b/pyptv/pyptv_gui.py index 8d08ef4..c2c6a9d 100644 --- a/pyptv/pyptv_gui.py +++ b/pyptv/pyptv_gui.py @@ -396,7 +396,9 @@ def copy_set_params(self, editor, object): paramset = object print(f" Copying set of parameters \n") print(f"paramset is {paramset.name}") - print(f"paramset id is {int(paramset.name.split('Run')[-1])}") + if 'Run' in paramset.name: + print(f"paramset id is {int(paramset.name.split('Run')[-1])}") + # print(f"paramset id is {int(paramset.name.split('Run')[-1])}") # print(f"experiment is {experiment}\n") i = 1 From 4d141b749b8ade2a93c38721648ad9ab94be169f Mon Sep 17 00:00:00 2001 From: Alex Liberzon Date: Thu, 20 Jun 2024 17:14:02 +0300 Subject: [PATCH 4/5] working with k1 only, need to make it generic --- pyptv/calibration_gui.py | 116 ++++++++++++++++++++++----------------- pyptv/code_editor.py | 8 +-- 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/pyptv/calibration_gui.py b/pyptv/calibration_gui.py index 779bd8f..6bbaa55 100644 --- a/pyptv/calibration_gui.py +++ b/pyptv/calibration_gui.py @@ -878,8 +878,9 @@ def _button_fine_orient_fired(self): op.shear, ] + # set flags for cc, xh, yh only flags = [] - for name, op_name in zip(names, op_names): + for name, op_name in zip(names[:3], op_names[:3]): if op_name == 1: flags.append(name) @@ -974,29 +975,33 @@ def _button_fine_orient_fired(self): else: targs = self.sorted_targs[i_cam] - try: - residuals, targ_ix, err_est = full_calibration( - self.cals[i_cam], - self.cal_points["pos"], - targs, - self.cpar, - flags, - ) - except BaseException as exc: - print(f"Error in full_calibration: {exc} go to scipy") + # try: + print(f"Calibrate only external and flags: {flags} \n") + residuals, targ_ix, err_est = full_calibration( + self.cals[i_cam], + self.cal_points["pos"], + targs, + self.cpar, + flags, + ) + + # let's try only k1 + if op_names[3]: + print(f"k1 flag is raised, going to scipy") from scipy.optimize import minimize cal = self.cals[i_cam] - print("Before: \n") + print("start with \n") - x0 = np.concatenate([ - cal.get_pos(), - cal.get_angles(), - cal.get_primary_point(), - cal.get_radial_distortion(), - cal.get_decentering(), - cal.get_affine(), - ]) + # x0 = np.concatenate([ + # cal.get_pos(), + # cal.get_angles(), + # cal.get_primary_point(), + # cal.get_radial_distortion(), + # cal.get_decentering(), + # cal.get_affine(), + # ]) + x0 = cal.get_radial_distortion()[0] print(x0) sol = minimize(self._error_function, x0, @@ -1006,12 +1011,19 @@ def _button_fine_orient_fired(self): self.cpar ), method='Nelder-Mead', - tol=1e-4, + tol=1e-6, + options={'disp':True}, ) print(f"Difference: {sol.x - x0}") print("Before: \n") print(self.cals[i_cam]) - self._array_to_calibration(sol.x, self.cals[i_cam]) + tmp = cal.get_radial_distortion() + tmp[0] = sol.x + cal.set_radial_distortion(tmp) + print(cal.get_radial_distortion()) + self.cals[i_cam] = cal + + # self._array_to_calibration(sol.x, self.cals[i_cam]) self._project_cal_points(i_cam) @@ -1021,7 +1033,9 @@ def _button_fine_orient_fired(self): all_detected, self.cpar ) - + + residuals /= 100 + print("\n Calibration using scipy.optimize.minimize \n") targ_ix = np.arange(len(all_detected)) @@ -1075,14 +1089,8 @@ def _array_to_calibration(self, x:np.ndarray, cal:Calibration) -> None: return None def _calibration_to_array(self, cal:Calibration) -> np.ndarray: - print(cal.get_pos()) - print(cal.get_angles()) - print(cal.get_primary_point()) - print(cal.get_radial_distortion()) - print(cal.get_decentering()) - print(cal.get_affine()) - - return np.concatenate([ + """ Convert calibration object to array """ + tmp = np.concatenate([ cal.get_pos(), cal.get_angles(), cal.get_primary_point(), @@ -1090,6 +1098,8 @@ def _calibration_to_array(self, cal:Calibration) -> np.ndarray: cal.get_decentering(), cal.get_affine(), ]) + + return tmp def _error_function(self, x, cal, XYZ, xy, cpar): """Error function for scipy.optimize.minimize. @@ -1104,20 +1114,6 @@ def _error_function(self, x, cal, XYZ, xy, cpar): Returns: float: Error value. """ - # cal.set_pos(x[:3]) - # cal.set_angles(x[3:6]) - # cal.set_primary_point(x[6:9]) - # # cal.set_radial_distortion(x[9:12]) - # # cal.set_decentering(x[12:14]) - # # cal.set_affine_distortion(x[14:]) - - # targets = convert_arr_metric_to_pixel( - # image_coordinates(XYZ, cal, cpar.get_multimedia_params()), - # cpar - # ) - - # err = np.sum((xy[:,1:] - targets)**2) - residuals = self._residuals(x, cal, XYZ, xy, cpar) err = np.sum(residuals**2) @@ -1134,14 +1130,32 @@ def _residuals(self, x, cal, XYZ, xy, cpar): cpar (CPar): Camera parameters. Returns: - float: Error value. + residuals: Distortion in pixels """ - cal.set_pos(x[:3]) - cal.set_angles(x[3:6]) - cal.set_primary_point(x[6:9]) + # tmp = x.copy() + + # x = np.concatenate([ + # cal.get_pos(), + # cal.get_angles(), + # cal.get_primary_point(), + # cal.get_radial_distortion(), + # cal.get_decentering(), + # cal.get_affine(), + # ]) + + # change only one thing: + # x[3] = tmp[3] + + # cal.set_pos(x[:3]) + # cal.set_angles(x[3:6]) + # cal.set_primary_point(x[6:9]) # cal.set_radial_distortion(x[9:12]) # cal.set_decentering(x[12:14]) - # cal.set_affine_distortion(x[14:]) + # cal.set_affine_trans(x[14:]) + + tmp = cal.get_radial_distortion() + tmp[0] = x + cal.set_radial_distortion(tmp) targets = convert_arr_metric_to_pixel( image_coordinates(XYZ, cal, cpar.get_multimedia_params()), @@ -1344,7 +1358,7 @@ def _read_cal_points(self): if len(sys.argv) == 1: active_path = Path("../test_cavity/parametersRun1") - active_path = Path("/home/user/Downloads/rbc300/parametersMultiPlain") + active_path = Path("/home/user/Downloads/rbc300/parametersMultiPlane") else: active_path = Path(sys.argv[0]) diff --git a/pyptv/code_editor.py b/pyptv/code_editor.py index 38bc1dc..6f9cab5 100644 --- a/pyptv/code_editor.py +++ b/pyptv/code_editor.py @@ -31,11 +31,11 @@ def get_path(filename): def get_code(path: Path): """ Read the code from the file """ - print(f"Read from {path}: {path.exists()}") + # print(f"Read from {path}: {path.exists()}") with open(path, "r", encoding="utf-8") as f: retCode = f.read() - print(retCode) + # print(retCode) return retCode @@ -58,8 +58,8 @@ class codeEditor(HasTraits): def _save_button_fired(self): with open(self.file_Path, "w", encoding="utf-8") as f: - print(f"Saving to {self.file_Path}") - print(f"Code: {self._Code}") + # print(f"Saving to {self.file_Path}") + # print(f"Code: {self._Code}") f.write(self._Code) print(f"Saved to {self.file_Path}") From be6752502238d08076e19870dd491229329f6508 Mon Sep 17 00:00:00 2001 From: Alex Liberzon Date: Thu, 20 Jun 2024 20:00:27 +0300 Subject: [PATCH 5/5] fully working version for multiplane calibration with provided values and additional parameters milestone achieved for the proPTV, MyPTV, OpenPTV benchmarking - use markers as xy, XYZ and convert them to .crd and .fix files - use multiplane combine to read those files in combined mode - use fast C calibration for external parameters, back focal point (cc, xh, yh) - use scipy.optimize.minimize to the radial distortion, decentering (p1,p2) and affine transformation (scale, shear) \ --- pyptv/calibration_gui.py | 233 ++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 116 deletions(-) diff --git a/pyptv/calibration_gui.py b/pyptv/calibration_gui.py index 6bbaa55..69e0eed 100644 --- a/pyptv/calibration_gui.py +++ b/pyptv/calibration_gui.py @@ -47,6 +47,7 @@ from pyptv import ptv, parameter_gui, parameters as par +from scipy.optimize import minimize # ------------------------------------------- class ClickerTool(ImageInspectorTool): @@ -976,7 +977,7 @@ def _button_fine_orient_fired(self): targs = self.sorted_targs[i_cam] # try: - print(f"Calibrate only external and flags: {flags} \n") + print(f"First calibrate only external and flags: {flags} \n") residuals, targ_ix, err_est = full_calibration( self.cals[i_cam], self.cal_points["pos"], @@ -985,59 +986,76 @@ def _button_fine_orient_fired(self): flags, ) - # let's try only k1 - if op_names[3]: - print(f"k1 flag is raised, going to scipy") - - from scipy.optimize import minimize - cal = self.cals[i_cam] - print("start with \n") - - # x0 = np.concatenate([ - # cal.get_pos(), - # cal.get_angles(), - # cal.get_primary_point(), - # cal.get_radial_distortion(), - # cal.get_decentering(), - # cal.get_affine(), - # ]) - x0 = cal.get_radial_distortion()[0] - print(x0) - sol = minimize(self._error_function, - x0, - args=(cal, + # this chunk optimizes for radial distortion + if np.any(op_names[3:6]): + sol = minimize(self._residuals_k, + self.cals[i_cam].get_radial_distortion(), + args=(self.cals[i_cam], all_known, all_detected, self.cpar ), method='Nelder-Mead', - tol=1e-6, + tol=1e-11, options={'disp':True}, ) - print(f"Difference: {sol.x - x0}") - print("Before: \n") - print(self.cals[i_cam]) - tmp = cal.get_radial_distortion() - tmp[0] = sol.x - cal.set_radial_distortion(tmp) - print(cal.get_radial_distortion()) - self.cals[i_cam] = cal - - # self._array_to_calibration(sol.x, self.cals[i_cam]) - self._project_cal_points(i_cam) - - - residuals = self._residuals(sol.x, - self.cals[i_cam], - all_known, - all_detected, - self.cpar - ) - - residuals /= 100 - - print("\n Calibration using scipy.optimize.minimize \n") - targ_ix = np.arange(len(all_detected)) + radial = sol.x + self.cals[i_cam].set_radial_distortion(radial) + else: + radial = self.cals[i_cam].get_radial_distortion() + + if np.any(op_names[5:8]): + # now decentering + sol = minimize(self._residuals_p, + self.cals[i_cam].get_decentering(), + args=(self.cals[i_cam], + all_known, + all_detected, + self.cpar + ), + method='Nelder-Mead', + tol=1e-11, + options={'disp':True}, + ) + decentering = sol.x + self.cals[i_cam].set_decentering(decentering) + else: + decentering = self.cals[i_cam].get_decentering() + + if np.any(op_names[8:]): + # now affine + sol = minimize(self._residuals_s, + self.cals[i_cam].get_affine(), + args=(self.cals[i_cam], + all_known, + all_detected, + self.cpar + ), + method='Nelder-Mead', + tol=1e-11, + options={'disp':True}, + ) + affine = sol.x + self.cals[i_cam].set_affine_trans(affine) + + else: + affine = self.cals[i_cam].get_affine() + + + # Now project and estimate full residuals + self._project_cal_points(i_cam) + + residuals = self._residuals_combined( + np.r_[radial, decentering, affine], + self.cals[i_cam], + all_known, + all_detected, + self.cpar + ) + + residuals /= 100 + + targ_ix = np.arange(len(all_detected)) # save the results from self.cals[i_cam] self._write_ori(i_cam, addpar_flag=True) @@ -1077,50 +1095,24 @@ def _button_fine_orient_fired(self): self.status_text = "Orientation finished." -# Insert parts for scipy.optimize calibration -# - def _array_to_calibration(self, x:np.ndarray, cal:Calibration) -> None: - cal.set_pos(x[:3]) - cal.set_angles(x[3:6]) - cal.set_primary_point(x[6:9]) - cal.set_radial_distortion(x[9:12]) - cal.set_decentering(x[12:14]) - cal. set_affine_trans(x[14:]) - return None - - def _calibration_to_array(self, cal:Calibration) -> np.ndarray: - """ Convert calibration object to array """ - tmp = np.concatenate([ - cal.get_pos(), - cal.get_angles(), - cal.get_primary_point(), - cal.get_radial_distortion(), - cal.get_decentering(), - cal.get_affine(), - ]) - - return tmp - - def _error_function(self, x, cal, XYZ, xy, cpar): - """Error function for scipy.optimize.minimize. - - Args: - x (array-like): Array of parameters. - cal (Calibration): Calibration object. - XYZ (array-like): 3D coordinates. - xy (array-like): 2D image coordinates. - cpar (CPar): Camera parameters. - - Returns: - float: Error value. - """ - residuals = self._residuals(x, cal, XYZ, xy, cpar) - err = np.sum(residuals**2) - - return err + # def _error_function(self, x, cal, XYZ, xy, cpar): + # """Error function for scipy.optimize.minimize. + + # Args: + # x (array-like): Array of parameters. + # cal (Calibration): Calibration object. + # XYZ (array-like): 3D coordinates. + # xy (array-like): 2D image coordinates. + # cpar (CPar): Camera parameters. + + # Returns: + # float: Error value. + # """ + # residuals = self._residuals_radial(x, cal, XYZ, xy, cpar) + # return np.sum(residuals**2) - def _residuals(self, x, cal, XYZ, xy, cpar): - """Error function for scipy.optimize.minimize. + def _residuals_k(self, x, cal, XYZ, xy, cpar): + """Residuals due to radial distortion Args: x (array-like): Array of parameters. @@ -1132,39 +1124,48 @@ def _residuals(self, x, cal, XYZ, xy, cpar): Returns: residuals: Distortion in pixels """ - # tmp = x.copy() - - # x = np.concatenate([ - # cal.get_pos(), - # cal.get_angles(), - # cal.get_primary_point(), - # cal.get_radial_distortion(), - # cal.get_decentering(), - # cal.get_affine(), - # ]) - - # change only one thing: - # x[3] = tmp[3] - - # cal.set_pos(x[:3]) - # cal.set_angles(x[3:6]) - # cal.set_primary_point(x[6:9]) - # cal.set_radial_distortion(x[9:12]) - # cal.set_decentering(x[12:14]) - # cal.set_affine_trans(x[14:]) - - tmp = cal.get_radial_distortion() - tmp[0] = x - cal.set_radial_distortion(tmp) + cal.set_radial_distortion(x) + targets = convert_arr_metric_to_pixel( + image_coordinates(XYZ, cal, cpar.get_multimedia_params()), + cpar + ) + residuals = xy[:,1:] - targets + return np.sum(residuals**2) + + def _residuals_p(self, x, cal, XYZ, xy, cpar): + """Residuals due to decentering """ + cal.set_decentering(x) + targets = convert_arr_metric_to_pixel( + image_coordinates(XYZ, cal, cpar.get_multimedia_params()), + cpar + ) + residuals = xy[:,1:] - targets + return np.sum(residuals**2) + + def _residuals_s(self, x, cal, XYZ, xy, cpar): + """Residuals due to decentering """ + cal.set_affine_trans(x) targets = convert_arr_metric_to_pixel( image_coordinates(XYZ, cal, cpar.get_multimedia_params()), cpar ) + residuals = xy[:,1:] - targets + return np.sum(residuals**2) + + def _residuals_combined(self, x, cal, XYZ, xy, cpar): + """Combined residuals """ + cal.set_radial_distortion(x[:3]) + cal.set_decentering(x[3:5]) + cal.set_affine_trans(x[5:]) + + targets = convert_arr_metric_to_pixel( + image_coordinates(XYZ, cal, cpar.get_multimedia_params()), + cpar + ) residuals = xy[:,1:] - targets - return residuals - + return residuals def _write_ori(self, i_cam, addpar_flag=False): """Writes ORI and ADDPAR files for a single calibration result