Last active
November 17, 2019 16:28
-
-
Save kohyuk91/9df1c8a6903931f2c8ab7cb6bd532d95 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Name: | |
| # easy_dewarp.py | |
| # | |
| # Author: | |
| # Hyuk Ko ([email protected]) | |
| # | |
| # | |
| # | |
| # 3DE4.script.name: Easy Dewarp... | |
| # | |
| # 3DE4.script.version: v1.0 | |
| # | |
| # 3DE4.script.gui: Main Window::Python | |
| # | |
| # 3DE4.script.comment: Export Dewarped images with Warp4 | |
| # | |
| # MODIFY THIS AT YOUR OWN RISK | |
| import os | |
| import sys | |
| import math | |
| import time | |
| import string | |
| import platform | |
| import subprocess | |
| import multiprocessing | |
| import multiprocessing.pool | |
| from functools import partial | |
| TDE4_PATH = tde4.get3DEInstallPath() | |
| OS = platform.system() | |
| if OS == "Windows": | |
| WARP4_PATH = r"{0}\bin\warp4.exe".format(TDE4_PATH) | |
| DIRECTORY_SEPARATOR = "\\" | |
| else: | |
| WARP4_PATH = "{0}/bin/warp4".format(TDE4_PATH) | |
| DIRECTORY_SEPARATOR = "/" | |
| # /py_scripts/calc_bounding_box_for_dynamic_distortion.py | |
| class bbdld_bounding_box: | |
| def __init__(self): | |
| self._x_min = float("inf") | |
| self._x_max = float("-inf") | |
| self._y_min = float("inf") | |
| self._y_max = float("-inf") | |
| # Extend so that it is symmetric around (cx,cy) | |
| def symmetrize(self,cx,cy): | |
| if cx - self._x_min > self._x_max - cx: | |
| self._x_max = 2.0 * cx - self._x_min | |
| else: | |
| self._x_min = 2.0 * cx - self._x_max | |
| if cy - self._y_min > self._y_max - cy: | |
| self._y_max = 2.0 * cy - self._y_min | |
| else: | |
| self._y_min = 2.0 * cy - self._y_max | |
| # Extend the bounding box so that it contains (x,y) | |
| def extend(self,x,y): | |
| self._x_min = min(self._x_min,float(x)) | |
| self._x_max = max(self._x_max,float(x)) | |
| self._y_min = min(self._y_min,float(y)) | |
| self._y_max = max(self._y_max,float(y)) | |
| # Symmetric extension around (cx,cy) | |
| def extend_symm(self,x,y,cx,cy): | |
| self.extend(x,y) | |
| self.symmetrize(cx,cy) | |
| # Scale, multiply x and y by some positiv number | |
| def scale(self,sx,sy): | |
| self._x_min *= sx | |
| self._x_max *= sx | |
| self._y_min *= sy | |
| self._y_max *= sy | |
| # Convenient for pixel coordinates (ignore float artefacs, therefore 1e-12 thingees) | |
| def extend_to_integer(self): | |
| self._x_min = math.floor(self._x_min + 1e-12) | |
| self._x_max = math.ceil(self._x_max - 1e-12) | |
| self._y_min = math.floor(self._y_min + 1e-12) | |
| self._y_max = math.ceil(self._y_max - 1e-12) | |
| # Properties | |
| def dx(self): | |
| return self._x_max - self._x_min | |
| def dy(self): | |
| return self._y_max - self._y_min | |
| def x_min(self): | |
| return self._x_min | |
| def x_max(self): | |
| return self._x_max | |
| def y_min(self): | |
| return self._y_min | |
| def y_max(self): | |
| return self._y_max | |
| def __str__(self): | |
| return "[" + str(self._x_min) + "," + str(self._x_max) + "," + str(self._y_min) + "," + str(self._y_max) + "]" | |
| def bbdld_compute_bounding_box(): | |
| # List of selected cameras | |
| id_cam = tde4.getCurrentCamera() | |
| # We genererate a number of samples around the image in normalized coordinates. | |
| # These samples are later unwarped, and the unwarped points | |
| # will be used to create a bounding box. In general, it is *not* sufficient to | |
| # undistort only the corners, because distortion might be moustache-shaped. | |
| # This is our list of samples: | |
| warped = [] | |
| for i in range(10): | |
| warped.append([i / 10.0,0.0]) | |
| warped.append([(i + 1) / 10.0,1.0]) | |
| warped.append([0.0,i / 10.0]) | |
| warped.append([1.0,(i + 1) / 10.0]) | |
| # Run through sequence cameras | |
| if tde4.getCameraType(id_cam) == "SEQUENCE": | |
| name = tde4.getCameraName(id_cam) | |
| # The lens of this sequence | |
| id_lens = tde4.getCameraLens(id_cam) | |
| # Lens center offset as given in GUI | |
| lco = [tde4.getLensLensCenterX(id_lens),tde4.getLensLensCenterY(id_lens)] | |
| # The lens center is by definition the fixed point of the distortion mapping. | |
| elc = [0.5 + lco[0],0.5 + lco[1]] | |
| # Image size | |
| w_px = tde4.getCameraImageWidth(id_cam) | |
| h_px = tde4.getCameraImageHeight(id_cam) | |
| # The bounding boxes for non-symmetrized and symmetrized cased. | |
| bb_nonsymm = bbdld_bounding_box() | |
| bb_symm = bbdld_bounding_box() | |
| # Run through the frames of this camera | |
| n_frames = tde4.getCameraNoFrames(id_cam) | |
| for i_frame in range(n_frames): | |
| # 3DE4 counts from 1. | |
| frame = i_frame + 1 | |
| # Now we undistort all edge points for the given | |
| # camera and frame and extend the bounding boxes. | |
| for p in warped: | |
| p_unwarped = tde4.removeDistortion2D(id_cam,frame,p) | |
| # Accumulate bounding boxes | |
| bb_nonsymm.extend(p_unwarped[0],p_unwarped[1]) | |
| bb_symm.extend_symm(p_unwarped[0],p_unwarped[1],elc[0],elc[1]) | |
| # Scale to pixel coordinates and extend to pixel-aligned values | |
| bb_nonsymm.scale(w_px,h_px) | |
| bb_nonsymm.extend_to_integer() | |
| # Image width and height for the non-symmetrized case | |
| w_nonsymm_px = bb_nonsymm.dx() | |
| h_nonsymm_px = bb_nonsymm.dy() | |
| # Lower left corner for the symmetrized case. This tells us | |
| # how the undistorted image is related to the distorted image. | |
| x_nonsymm_px = bb_nonsymm.x_min() | |
| y_nonsymm_px = bb_nonsymm.y_min() | |
| # Scale to pixel coordinates and extend to pixel-aligned values | |
| bb_symm.scale(w_px,h_px) | |
| bb_symm.extend_to_integer() | |
| # Image width and height for the symmetrized case | |
| w_symm_px = bb_symm.dx() | |
| h_symm_px = bb_symm.dy() | |
| # Lower left corner for the symmetrized case. This tells us | |
| # how the undistorted image is related to the distorted image. | |
| x_symm_px = bb_symm.x_min() | |
| y_symm_px = bb_symm.y_min() | |
| return int(w_symm_px), int(h_symm_px) | |
| # /py_scripts/export_nuke_LD_3DE4_Lens_Distortion.py | |
| def getLDmodelParameterList(model): | |
| l = [] | |
| for p in range(tde4.getLDModelNoParameters(model)): | |
| l.append(tde4.getLDModelParameterName(model, p)) | |
| return l | |
| def update_overscan_resolution(): | |
| tde4.setWidgetValue(req, 'overscan_width', str(int(math.ceil(float(tde4.getWidgetValue(req, "overscan_value")) * w_px)))) # Always Even Number | |
| tde4.setWidgetValue(req, 'overscan_height', str(int(math.ceil(float(tde4.getWidgetValue(req, "overscan_value")) * h_px)))) # Always Even Number | |
| def update_bbox_fg_color(): | |
| if int(tde4.getWidgetValue(req, "overscan_width")) < int(tde4.getWidgetValue(req, "bounding_box_width")) or int(tde4.getWidgetValue(req, "overscan_height")) < int(tde4.getWidgetValue(req, "bounding_box_height")): | |
| tde4.setWidgetFGColor(req, 'bounding_box_width', 1.0, 0.0, 0.0) | |
| tde4.setWidgetFGColor(req, 'bounding_box_height', 1.0, 0.0, 0.0) | |
| else: | |
| tde4.setWidgetFGColor(req, 'bounding_box_width', 0.0, 1.0, 0.0) | |
| tde4.setWidgetFGColor(req, 'bounding_box_height', 0.0, 1.0, 0.0) | |
| def _overscan_callback(requester, widget, action): | |
| if widget == 'minus_05': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)-0.5)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'minus_01': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)-0.1)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'minus_005': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)-0.05)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'plus_005': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)+0.05)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'plus_01': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)+0.1)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'plus_05': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(float(val)+0.5)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| if widget == 'reset': | |
| val = tde4.getWidgetValue(req, "overscan_value") | |
| tde4.setWidgetValue(req, 'overscan_value', str(1.0)) | |
| update_overscan_resolution() | |
| update_bbox_fg_color() | |
| id_cam = tde4.getCurrentCamera() | |
| id_lens = tde4.getCameraLens(id_cam) | |
| model = tde4.getLensLDModel(id_lens) | |
| num_frames = tde4.getCameraNoFrames(id_cam) | |
| w_fb_cm = tde4.getLensFBackWidth(id_lens) | |
| h_fb_cm = tde4.getLensFBackHeight(id_lens) | |
| lco_x_cm = tde4.getLensLensCenterX(id_lens) | |
| lco_y_cm = tde4.getLensLensCenterY(id_lens) | |
| pxa = tde4.getLensPixelAspect(id_lens) | |
| w_px = tde4.getCameraImageWidth(id_cam) | |
| h_px = tde4.getCameraImageHeight(id_cam) | |
| bbx_w_px, bbx_h_px = bbdld_compute_bounding_box() | |
| footagePath = tde4.getCameraPath(id_cam) # "/path/to/file/trackboy.####.jpg" | |
| footageDir, footageFilename = os.path.split(footagePath) # ["/path/to/file", "trackboy.####.jpg"] | |
| footageHead, footageTail = footageFilename.split(".", 1) # ["trackboy", "####.jpg"] | |
| dewarpedDirectory = footageDir | |
| dewarpedFilename = "{0}_dewarped.{1}".format(footageHead, footageTail) | |
| ## GUI | |
| req = tde4.createCustomRequester() | |
| tde4.addFileWidget(req, "dewarped_directory", "Dewarped Directory", "*", dewarpedDirectory) | |
| tde4.addTextFieldWidget(req, "dewarped_filename", "Dewarped Filename", dewarpedFilename) | |
| tde4.setWidgetSensitiveFlag(req, 'dewarped_filename', 0) | |
| tde4.addSeparatorWidget(req, 'separator1') ####################################### | |
| tde4.addTextFieldWidget(req, 'overscan_value', 'Overscan Value', "1.0") | |
| tde4.setWidgetLinks(req,"overscan_value","separator1","", "separator1", "") | |
| tde4.setWidgetAttachModes(req, "overscan_value","ATTACH_AS_IS","ATTACH_NONE","ATTACH_AS_IS","ATTACH_AS_IS") | |
| tde4.setWidgetSensitiveFlag(req, 'overscan_value', 0) | |
| tde4.setWidgetSize(req, "overscan_value", 80,20) | |
| tde4.addButtonWidget(req, "minus_05", "-0.5") | |
| tde4.setWidgetLinks(req,"minus_05","overscan_value","", "overscan_value", "") | |
| tde4.setWidgetAttachModes(req, "minus_05","ATTACH_AS_IS","ATTACH_NONE","ATTACH_AS_IS","ATTACH_AS_IS") | |
| tde4.addButtonWidget(req, "minus_01", "-0.1") | |
| tde4.setWidgetOffsets(req,"minus_01",5,0,0,0) | |
| tde4.setWidgetLinks(req,"minus_01","minus_05","", "minus_05", "") | |
| tde4.setWidgetAttachModes(req,"minus_01","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.addButtonWidget(req, "minus_005", "-0.05") | |
| tde4.setWidgetOffsets(req,"minus_005",5,0,0,0) | |
| tde4.setWidgetLinks(req,"minus_005","minus_01","", "minus_01", "") | |
| tde4.setWidgetAttachModes(req,"minus_005","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.addButtonWidget(req, "plus_005", "+0.05") | |
| tde4.setWidgetOffsets(req,"plus_005",5,0,0,0) | |
| tde4.setWidgetLinks(req,"plus_005","minus_005","", "minus_005", "") | |
| tde4.setWidgetAttachModes(req,"plus_005","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.addButtonWidget(req, "plus_01", "+0.1") | |
| tde4.setWidgetOffsets(req,"plus_01",5,0,0,0) | |
| tde4.setWidgetLinks(req,"plus_01","plus_005","", "plus_005", "") | |
| tde4.setWidgetAttachModes(req,"plus_01","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.addButtonWidget(req, "plus_05", "+0.5") | |
| tde4.setWidgetOffsets(req,"plus_05",5,0,0,0) | |
| tde4.setWidgetLinks(req,"plus_05","plus_01","", "plus_01", "") | |
| tde4.setWidgetAttachModes(req,"plus_05","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.addButtonWidget(req, "reset", "Reset") | |
| tde4.setWidgetOffsets(req,"reset",5,0,0,0) | |
| tde4.setWidgetLinks(req,"reset","plus_05","", "plus_05", "") | |
| tde4.setWidgetAttachModes(req,"reset","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.setWidgetBGColor(req, 'reset', 0.5, 0.0, 0.0) | |
| tde4.addTextFieldWidget(req, 'overscan_width', 'Overscan Resolution', str(w_px)) | |
| tde4.setWidgetLinks(req,"overscan_width","minus_05","", "minus_05", "") | |
| tde4.setWidgetAttachModes(req, "overscan_width","ATTACH_AS_IS","ATTACH_NONE","ATTACH_AS_IS","ATTACH_AS_IS") | |
| tde4.setWidgetSize(req, "overscan_width", 80,20) | |
| tde4.setWidgetSensitiveFlag(req, 'overscan_width', 0) | |
| tde4.addTextFieldWidget(req, 'overscan_height', 'X', str(h_px)) | |
| tde4.setWidgetOffsets(req,"overscan_height",25,0,0,0) | |
| tde4.setWidgetLinks(req,"overscan_height","overscan_width","", "overscan_width", "") | |
| tde4.setWidgetAttachModes(req,"overscan_height","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.setWidgetSize(req, "overscan_height", 80,20) | |
| tde4.setWidgetSensitiveFlag(req, 'overscan_height', 0) | |
| tde4.addTextFieldWidget(req, 'bounding_box_width', 'Bounding Box', str(bbx_w_px)) | |
| tde4.setWidgetLinks(req,"bounding_box_width","overscan_width","", "overscan_width", "") | |
| tde4.setWidgetAttachModes(req, "bounding_box_width","ATTACH_AS_IS","ATTACH_NONE","ATTACH_AS_IS","ATTACH_AS_IS") | |
| tde4.setWidgetSize(req, "bounding_box_width", 80,20) | |
| tde4.setWidgetSensitiveFlag(req, 'bounding_box_width', 0) | |
| tde4.addTextFieldWidget(req, 'bounding_box_height', 'X', str(bbx_h_px)) | |
| tde4.setWidgetOffsets(req,"bounding_box_height",25,0,0,0) | |
| tde4.setWidgetLinks(req,"bounding_box_height","bounding_box_width","", "bounding_box_width", "") | |
| tde4.setWidgetAttachModes(req,"bounding_box_height","ATTACH_WIDGET","ATTACH_NONE","ATTACH_OPPOSITE_WIDGET","ATTACH_NONE") | |
| tde4.setWidgetSize(req, "bounding_box_height", 80,20) | |
| tde4.setWidgetSensitiveFlag(req, 'bounding_box_height', 0) | |
| # | |
| ## If "Negative Distortion" disable overscan. | |
| if bbx_w_px <= w_px and bbx_h_px <= h_px: | |
| tde4.setWidgetFGColor(req, 'overscan_value', 0.7, 0.7, 0.7) | |
| tde4.setWidgetFGColor(req, 'overscan_width', 0.7, 0.7, 0.7) | |
| tde4.setWidgetFGColor(req, 'overscan_height', 0.7, 0.7, 0.7) | |
| tde4.setWidgetFGColor(req, 'bounding_box_width', 0.7, 0.7, 0.7) | |
| tde4.setWidgetFGColor(req, 'bounding_box_height', 0.7, 0.7, 0.7) | |
| tde4.setWidgetSensitiveFlag(req, 'minus_05', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'minus_01', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'minus_005', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'plus_005', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'plus_01', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'plus_05', 0) | |
| tde4.setWidgetSensitiveFlag(req, 'reset', 0) | |
| else: | |
| tde4.setWidgetFGColor(req, 'bounding_box_width', 1.0, 0.0, 0.0) | |
| tde4.setWidgetFGColor(req, 'bounding_box_height', 1.0, 0.0, 0.0) | |
| # | |
| ## Callback | |
| tde4.setWidgetCallbackFunction(req, 'minus_05', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'minus_01', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'minus_005', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'plus_005', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'plus_01', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'plus_05', '_overscan_callback') | |
| tde4.setWidgetCallbackFunction(req, 'reset', '_overscan_callback') | |
| ret = tde4.postCustomRequester(req, "Easy Dewarp...", 900, 240, "Ok", "Cancel") | |
| if ret==1: | |
| cmd_list = [] | |
| for frame in range(1, num_frames+1): | |
| focal = tde4.getCameraFocalLength(id_cam, frame) | |
| focus = tde4.getCameraFocus(id_cam, frame) | |
| inPath = tde4.getCameraFrameFilepath(id_cam, frame) | |
| inDirectory, inFilename = os.path.split(inPath) | |
| inHead, inTail = inFilename.split(".", 1) | |
| outPath = r"{0}{1}{2}_dewarped.{3}".format(tde4.getWidgetValue(req, 'dewarped_directory'), DIRECTORY_SEPARATOR, inHead, inTail) | |
| parameters = "" | |
| for para in getLDmodelParameterList(model): | |
| parameters += str(tde4.getLensLDAdjustableParameter(id_lens, para, focal, focus)) + " " | |
| cmd = r'{0} -in {1} -out {2} -action remove_distortion -model "{3}" -parameters {4} -pixel_aspect {5} -lco {6} {7} -filmback {8} {9} -focal_length {10} -overscan {11} {12}'.format(WARP4_PATH, inPath, outPath, model, parameters, pxa, lco_x_cm, lco_y_cm, w_fb_cm, h_fb_cm, focal, tde4.getWidgetValue(req, 'overscan_width'), tde4.getWidgetValue(req, 'overscan_height')) | |
| cmd_list.append(cmd) | |
| p = multiprocessing.pool.ThreadPool(multiprocessing.cpu_count()-1) # Used ThreadPool instead of Pool. OS compat reasons... | |
| start = time.time() | |
| tde4.postProgressRequesterAndContinue("Easy Dewarp...", "Dewarping Frame 1 out of {0}...".format(len(cmd_list)), len(cmd_list), "Stop") | |
| for i, returncode in enumerate(p.imap(partial(subprocess.call, shell=True), cmd_list)): | |
| tde4.updateProgressRequester(i+1,"Dewarping Frame {0} out of {1}...".format(i+1, len(cmd_list))) | |
| if returncode != 0: | |
| print("%d command failed: %d" % (i, returncode)) | |
| tde4.unpostProgressRequester() | |
| print 'Time elapsed: %s' % (time.time() - start) | |
| # Copy miscellaneous data to Clipboard | |
| cb_string = r"{0}{1}{2} {3} {4} {5} {6}".format(tde4.getWidgetValue(req, 'dewarped_directory'), DIRECTORY_SEPARATOR, dewarpedFilename, w_px, h_px, tde4.getWidgetValue(req, 'overscan_width'), tde4.getWidgetValue(req, 'overscan_height')) | |
| tde4.setClipboardString(cb_string) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment