Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ gore/__pycache__/gore2.cpython-38.pyc
gore/__pycache__/gore2.cpython-38.pyc
*.pyc
installer build instructions.rtf
*.*~
60 changes: 23 additions & 37 deletions Example.ipynb

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions Interface.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "fdabcb6092b24a218fb1675b0da82932",
"model_id": "f0817aeeccfb4f548e68f63d6673b9c0",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -32,12 +32,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "0a97cefd82f4450d991cb23b70e0c32d",
"model_id": "ff7d69a4af1445609275c001ebb10494",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"FileUpload(value={}, description='Upload')"
"FileUpload(value=(), description='Upload')"
]
},
"metadata": {},
Expand All @@ -46,7 +46,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "0eb7988bb0b0429c8812cf219cd78cfe",
"model_id": "692052b7e9e94d8e9b8b91d48c950fbd",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -60,7 +60,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "12f2022fa4684956af412dd3a750ad2e",
"model_id": "28bd3a5061964cadb42346bef678a670",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -74,7 +74,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e8be6cfcc1fd46099742f7d924c5cedb",
"model_id": "14ac1d50b49340ddb0a4f2651ced5371",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -88,7 +88,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "cdf2488628c5432d9c6a0e6c43276c41",
"model_id": "41ddf71278e749a787a2dce856bc4e96",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -102,7 +102,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "4388bdc3cf75461c8eca71f4111763a2",
"model_id": "7fa8824effc84658975eac161f702804",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -116,7 +116,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "0857de4fa1744b72b1490ea9c324a5ea",
"model_id": "708a2efbf56b4d5e81e4048dd71ad3d7",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -130,7 +130,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "6e0c8c8b0d3449a9a3f81c5da1e36dca",
"model_id": "a94397ee0faa4036b68cc6b82f604c97",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -144,7 +144,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "284644725f724e14a2a3e53eae47ee6a",
"model_id": "06d69ef80abf485191a40cd93f06c47d",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -158,7 +158,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "3480e3270d33424dbb49c1cbbdf614c6",
"model_id": "5e0b68d1701b40a1b29746549deb2187",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -172,12 +172,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "106b297129ce4dd4a56f4b5d1e6d897b",
"model_id": "8137707664824d818d42c570fd692b38",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output(layout=Layout(border='1px solid black'))"
"Output(layout=Layout(border_bottom='1px solid black', border_left='1px solid black', border_right='1px solid b…"
]
},
"metadata": {},
Expand Down
160 changes: 78 additions & 82 deletions gore/gore2.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,65 +300,77 @@ def make_polar (im,

return pole_stitched

def convert_to_rgb_with_background(im, background_colour):
"""
Converts an RGBA image to RGB by applying the specified background color to transparent areas.

im: Input image (PIL.Image)
background_colour: Background color as a tuple (R, G, B)

def swap (im,
phi_extent = mt.pi / 2,
lam_extent = mt.pi,
background_colour = (0, 0, 0, 0)):
Returns: Converted RGB image (PIL.Image)
"""
swap takes an equirectangular (plate-caree) projection of a certain
angular extent and rotates it about the y-axis, so the poles lie
if im.mode == "RGBA":
# Create a new background image
r, g, b, _ = background_colour
background = Image.new("RGB", im.size, (r, g, b))
# Paste the image onto the background using transparency
background.paste(im, mask=im.split()[3]) # Use the alpha channel as a mask
return background
else:
return im.convert("RGB")

def swap(im, phi_extent=mt.pi / 2, lam_extent=mt.pi, background_colour=(0, 0, 0, 0)):
"""
swap takes an equirectangular (plate-caree) projection of a certain
angular extent and rotates it about the y-axis, so the poles lie
at the equator and the equator becomes a meridian.
im: input image (ndarray)
phi_extent: latitudnal extent (float)
lam_extent: longitudnal extent (float)
background_colour: background colour to use beyond extent (R,G,B,A tuple)
returns: output image (ndarray)

im: Input image (ndarray)
phi_extent: Latitudinal extent (float)
lam_extent: Longitudinal extent (float)
background_colour: Background color to use beyond extent (R, G, B, A tuple)

Returns: Output image (ndarray)
"""

# calculate basic quantities
# Calculate basic quantities
h, w = im.shape[:2]
# calculate the angular extents

# Calculate the angular extents
phi_dst_min, phi_dst_max, lam_dst_min, lam_dst_max = -np.pi / 2, np.pi / 2, 0, 2 * np.pi
phi_src_min, phi_src_max, lam_src_min, lam_src_max = -phi_extent, phi_extent, -lam_extent, lam_extent
# create arrays of polar coordinates spanning the extent
phi_vector, lam_vector = np.linspace(phi_dst_min, phi_dst_max, h, dtype = np.float32), np.linspace(lam_dst_min, lam_dst_max, w, dtype = np.float32)

# Create arrays of polar coordinates spanning the extent
phi_vector, lam_vector = np.linspace(phi_dst_min, phi_dst_max, h, dtype=np.float32), np.linspace(lam_dst_min, lam_dst_max, w, dtype=np.float32)
lam_dst, phi_dst = np.meshgrid(lam_vector, phi_vector)
# prepare the rotation: this is a pi/2 rotation about the y-axis

# Prepare the rotation: this is a pi/2 rotation about the y-axis
phi_src = np.arcsin(np.clip(np.cos(lam_dst) * np.cos(phi_dst), -1, 1))
lam_src = np.arctan2(np.sin(lam_dst) * np.cos(phi_dst),-np.sin(phi_dst))
lam_src = np.arctan2(np.sin(lam_dst) * np.cos(phi_dst), -np.sin(phi_dst))
y_src = (phi_src - phi_src_min) * h / (phi_src_max - phi_src_min)
x_src = (lam_src - lam_src_min) * w / (lam_src_max - lam_src_min)
# create a background colour image
r, g, b, a = background_colour
backgroundImage = np.array(Image.new("RGB", (h, w), (r, g, b)))

# perform the remap
dst = cv2.remap(im, x_src, y_src, cv2.INTER_LINEAR, backgroundImage, cv2.BORDER_TRANSPARENT)


# Create a background color image
r, g, b, _ = background_colour
background_image = np.zeros((h, w, 3), dtype=np.uint8)
background_image[:, :] = (r, g, b)

# Perform the remap
dst = cv2.remap(im, x_src, y_src, cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(r, g, b))

return dst


def equi(im,
alpha_max,
focal_length = 24):
alpha_max):
"""
equi takes a fundus image and computes its equirectangular (plate caree)
projection assuming a simple spherical eye model
projection assuming a simple spherical eye model, with radius = 11mm
and focal length = 17mm

im input image (ndarray)

alpha_max angular size of the image from the centre (radians)

focal length default assumes a focal length of one eye diameter (mm)

returns: (
output image (ndarray),
lambda max (float),
Expand All @@ -369,22 +381,21 @@ def equi(im,

# basic quantities
ht,wd = im.shape[0:2]
d = focal_length

# subtract a small amount (1 degree) to avoid going off the edge
alpha_max -= deg2rad(1.0)
phi_max = lam_max = float(alpha_max)
phi_min, lam_min = -phi_max, -lam_max
Lp_max = d * np.sin(phi_max) / (np.cos(phi_max) + 1)
Lp_max = 17 * 11 * np.sin(phi_max) / (6 + 11 * np.cos(phi_max))

# prepare polar coordinate arrays that span the extent
phis = np.linspace(phi_min, phi_max, ht, dtype = np.float32)
lams = np.linspace(lam_min, lam_max, wd, dtype = np.float32)
phi, lam = np.meshgrid(phis, lams)

# calculate the source angular coordinates for each pair of destination coordinates
Lp_x = d * np.sin(phi) / (np.cos(phi) + 1)
Lp_y = d * np.sin(lam) / (np.cos(lam) + 1)
Lp_x = 17 * 11 * np.sin(phi) / (6 + 11 * np.cos(phi))
Lp_y = 17 * 11 * np.sin(lam) / (6 + 11 * np.cos(lam))

x = np.floor(Lp_x / Lp_max * ht / 2 + ht / 2)
y = np.floor(Lp_y / Lp_max * wd / 2 + wd / 2)
Expand Down Expand Up @@ -429,7 +440,6 @@ def polecap (im,


def make_rotary (im,
focal_length,
alpha_max,
num_gores,
phi_no_cut,
Expand All @@ -440,7 +450,6 @@ def make_rotary (im,
make_rotary master function to produce a gore net stitched at the pole

im: input image (PIL.Image)
focal_length: focal length (mm)
alpha_max: angular size of the image from the centre (radians)
num_gores: number of gores (integer)
projection: projection to use (constant)
Expand All @@ -454,8 +463,7 @@ def make_rotary (im,
signal.emit(Progress.EQUI.value)

# create the equirectangular (plate-caree) representation of the fundus
fundus_equi, lammax, phimax = equi(im = im,
focal_length = focal_length, alpha_max = alpha_max)
fundus_equi, lammax, phimax = equi(im = im, alpha_max = alpha_max)

if QThread.currentThread().isInterruptionRequested():
return
Expand Down Expand Up @@ -509,46 +517,34 @@ def make_rotary (im,
return fundus_rotary


def make_rotary_adjusted (image_path,
focal_length,
alpha_max,
num_gores,
phi_no_cut,
rotation,
quality,
alpha_limit = mt.pi,
projection = Projection.CASSINI,
background_colour = (0,0,0,0),
im = None):
def make_rotary_adjusted(image_path, alpha_max, num_gores, phi_no_cut, rotation, quality, alpha_limit=mt.pi, projection=Projection.CASSINI, background_colour=(0, 0, 0, 0), im=None):
"""
make_rotary_adjusted master function to produce a gore net stitched at
the pole, specifying desired quality and rotation

image_path: input image path
focal_length: focal length (mm)
alpha_max: angular size of the image from the centre (radians)
num_gores: number of gores (integer)
phi_no_cut: angle of "no-cut zone" (radians)
rotation: angle of rotation (radians)
quality: image quality (percentage)
alpha_limit: angular extent of gored region
projection: map projection to use (Projection class)
background_colour: background colour to use beyond fundus (R,G,B,A tuple)
im: input PIL image (overrides image_path)
make_rotary_adjusted Master function to produce a gore net stitched at
the pole, specifying desired quality and rotation.

image_path: Input image path
alpha_max: Angular size of the image from the center (radians)
num_gores: Number of gores (integer)
phi_no_cut: Angle of "no-cut zone" (radians)
rotation: Angle of rotation (radians)
quality: Image quality (percentage)
alpha_limit: Angular extent of gored region
projection: Map projection to use (Projection class)
background_colour: Background color to use beyond fundus (R, G, B, A tuple)
im: Input PIL image (overrides image_path)
"""

if im is None:
im = image_from_path(image_path)


# Apply quality resizing
im = deres_image(im, float(quality / 100))
if (rotation > 0):

# Apply rotation if specified
if rotation > 0:
im = rotate_image(im, rotation)

return make_rotary (im,
focal_length,
alpha_max,
num_gores,
phi_no_cut,
alpha_limit,
projection,
background_colour)

# Ensure the image has the correct background color for JPEG
im = convert_to_rgb_with_background(Image.fromarray(im), background_colour)

# Continue with the rotary creation process
return make_rotary(np.array(im), alpha_max, num_gores, phi_no_cut, alpha_limit, projection, background_colour)
1 change: 0 additions & 1 deletion gore/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ def get_inputs():

inputs = dict(
image_path = None,
focal_length = 24,
alpha_max = gore2.deg2rad(w_alpha_max.value) / 2,
num_gores = w_num_gores.value,
phi_no_cut = gore2.deg2rad(w_phi_no_cut.value) / 2,
Expand Down
5 changes: 2 additions & 3 deletions qt/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
from time import perf_counter

# Tuple to store major, minor and revision numbers
version = (0,1,1)
version = (0,2,0)

versionText = "Gore Sim Eye v{0}.{1}.{2}".format(*version)

aboutText = """
University of St Andrews 2023
University of St Andrews 2023 - 2025

Credits:

Expand Down Expand Up @@ -1007,7 +1007,6 @@ def clear_image(self):
def get_inputs(self):
# collect the inputs to the calculation as a dict
inputs = dict(image_path = self.imagePath,
focal_length = 24,
alpha_max = deg2rad(self.fundusImageSizeValue / 2), # account for difference in angle measurement in gore2
num_gores = self.numberOfGoresValue,
alpha_limit = deg2rad(self.retinalSizeValue / 2), # account for difference in angle measurement in gore2
Expand Down