コード例 #1
0
ファイル: starrynight.py プロジェクト: rodluger/starrynight
    def precompute(self, b, theta, bo, ro):
        # Ingest
        self.ingest(b, theta, bo, ro)

        # Illumination matrix
        self.IA1 = self.illum().dot(self.A1)

        # Get integration code & limits
        self.kappa, self.lam, self.xi, self.code = get_angles(
            self.b,
            self.theta,
            self.costheta,
            self.sintheta,
            self.bo,
            self.ro,
        )

        # Compute the three primitive integrals if necessary
        if self.code not in [
                FLUX_ZERO,
                FLUX_SIMPLE_OCC,
                FLUX_SIMPLE_REFL,
                FLUX_SIMPLE_OCC_REFL,
        ]:
            self.P = compute_P(self.ydeg + 1, self.bo, self.ro, self.kappa)
            self.Q = compute_Q(self.ydeg + 1, self.lam)
            self.T = compute_T(self.ydeg + 1, self.b, self.theta, self.xi)
        else:
            self.P = None
            self.Q = None
            self.T = None
コード例 #2
0
 def rotate_camera(self, dx, dy):
     dx = 2 * self.r * -dx
     dy = 2 * self.r * -dy
     self._direction, self._top = geometry.rotate(
         self.direction, self.top, dx, dy)
     self.direction = self.direction
     self.cache = None
     return map(lambda x: (x * 180 / math.pi) % 360,
                geometry.get_angles(self.direction))
コード例 #3
0
ファイル: numerical.py プロジェクト: rodluger/starrynight
    def precompute(self, b, theta, bo, ro):
        # Ingest
        self.ingest(b, theta, bo, ro)

        # Get integration code & limits
        self.kappa, self.lam, self.xi, self.code = get_angles(
            self.b, self.theta, self.costheta, self.sintheta, self.bo, self.ro)
        self.phi = self.kappa - np.pi / 2

        # Illumination matrix
        self.IA1 = self.illum().dot(self.A1)

        # Compute the three primitive integrals
        self.P = np.zeros((self.ydeg + 2)**2)
        self.Q = np.zeros((self.ydeg + 2)**2)
        self.T = np.zeros((self.ydeg + 2)**2)
        n = 0
        for l in range(self.ydeg + 2):
            for m in range(-l, l + 1):
                self.P[n] = self.Plm(l, m)
                self.Q[n] = self.Qlm(l, m)
                self.T[n] = self.Tlm(l, m)
                n += 1
コード例 #4
0
def app(input_file, output_file):
    gauge = process_image.Gauge()
    host = housekeeping.OS()
    # setup image
    reuse_circle = housekeeping.test_time()
    im = host.get_image(DEF.SAMPLE_PATH, input_file)
    gray_im = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
    prep = gray_im  # copy of grayscale image to be preprocessed for angle detection
    mask = np.zeros_like(prep)
    canvas = np.zeros_like(im)  # output image
    np.copyto(canvas, im)
    error_screen = np.zeros_like(im)
    height, width = gray_im.shape
    assert height == DEF.HEIGHT
    assert width == DEF.WIDTH

    # mask_bandpass is a mask of the gauge obtained from histogram peaks
    hist0, mask_bandpass = hist_an.bandpass(gray_im)
    # prep is binary image ready for line detection
    prep = process_image.blur_n_threshold(prep, do_blur=1)
    prep = process_image.erode_n_dilate(prep)
    # host.write_image(("mask_bandpass.jpg", mask_bandpass))
    # Hough transforms
    if reuse_circle:
        print "reusing previous circle"
        circle_x, circle_y, radius = housekeeping.return_circle()
        circle_x = int(circle_x)
        circle_y = int(circle_y)
    else:
        circles = hough_transforms.hough_c(gray_im)
        for c in circles[:1]:
            # scale factor makes applicable area slightly larger just to be safe we are not missing anything
            cv2.circle(mask, (c[0], c[1]), int(c[2] * DEF.CIRCLE_SCALE_FACTOR),
                       (255, 255, 255), -1)
            prep = cv2.bitwise_and(prep, mask)
            cv2.circle(canvas, (c[0], c[1]), c[2], (0, 255, 0), DEF.THICKNESS)
            circle_x = c[0]
            circle_y = c[1]
            radius = int(c[2])

    cv2.circle(mask, (int(circle_x), int(circle_y)),
               int(radius * DEF.CIRCLE_SCALE_FACTOR), (255, 255, 255), -1)
    cv2.circle(canvas, (int(circle_x), int(circle_y)), radius, (0, 255, 0),
               DEF.THICKNESS)
    cv2.circle(canvas, (int(circle_x), int(circle_y - 12)), 25, (0, 255, 0),
               DEF.THICKNESS)
    prep = cv2.bitwise_and(prep, mask)

    prep = cv2.bitwise_and(mask_bandpass, prep)
    thresholded = np.copy(prep)
    prep = process_image.find_contour(prep, draw=True)

    lines = hough_transforms.hough_l(prep)

    for line in lines[:10]:
        for (rho, theta) in line:
            # blue for infinite lines (only draw the 2 strongest)
            x0 = np.cos(theta) * rho
            y0 = np.sin(theta) * rho
            pt1 = (int(x0 + (height + width) * (-np.sin(theta))),
                   int(y0 + (height + width) * np.cos(theta)))
            pt2 = (int(x0 - (height + width) * (-np.sin(theta))),
                   int(y0 - (height + width) * np.cos(theta)))
            gauge.lines.append((pt1, pt2))
            gauge.angles_in_radians.append(theta)
            gauge.angles_in_degrees.append(geometry.rad_to_degrees(theta))
            # cv2.line(canvas, pt1, pt2, (255, 255, 0), DEF.THICKNESS / 3)
    # check standard deviation of angles to catch angle wrap-around (359 to 0)
    angle_std = np.std(gauge.angles_in_degrees)
    if angle_std > 50:
        for i in range(len(gauge.angles_in_degrees)):
            if gauge.angles_in_degrees[i] < 20:
                gauge.angles_in_degrees[i] += 180
                gauge.angles_in_radians[i] += np.pi
    angle_index = geometry.get_angles(gauge.angles_in_degrees)
    if len(gauge.lines) < 2:
        EX.error_output(error_screen,
                        err_msg="unable to find needle from lines")
        sys.exit("unable to find needle form lines")

    line_found = False
    for this_pair in angle_index:
        # print "angle1: ", gauge.angles_in_degrees[this_pair[0]]
        # print "angle2: ", gauge.angles_in_degrees[this_pair[1]]
        print this_pair
        line1 = geometry.make_line(gauge.lines[this_pair[0]])
        line2 = geometry.make_line(gauge.lines[this_pair[1]])
        intersecting_pt = geometry.intersection(line1, line2)
        if intersecting_pt is not None:
            print "Intersection detected:", intersecting_pt
            cv2.circle(canvas, intersecting_pt, 10, DEF.RED, 3)
        else:
            print "No single intersection point detected"

        # Although we found a line coincident with the gauge needle,
        # we need to find which of the two direction it is pointing
        # guess1 and guess2 are 180 degrees apart
        avg_theta = (gauge.angles_in_radians[this_pair[0]] +
                     gauge.angles_in_radians[this_pair[1]]) / 2
        guess1 = [avg_theta, (0, 0)]
        guess2 = [avg_theta + np.pi, (0, 0)]
        if guess2[0] > 2 * np.pi:  # in case adding pi made it greater than 2pi
            guess2[0] -= 2 * np.pi
        print "guess1: ", guess1[0]
        print "guess2: ", guess2[0]
        guess1[1] = (int(circle_x + radius * np.sin(guess1[0])),
                     int(circle_y - radius * np.cos(guess1[0])))
        guess2[1] = (int(circle_x + radius * np.sin(guess2[0])),
                     int(circle_y - radius * np.cos(guess2[0])))
        # find the distance between our guess and intersection of gauge needle lines
        # the guess that is closer to the intersection is the correct one
        dist1 = math.hypot(intersecting_pt[0] - guess1[1][0],
                           intersecting_pt[1] - guess1[1][1])
        dist2 = math.hypot(intersecting_pt[0] - guess2[1][0],
                           intersecting_pt[1] - guess2[1][1])
        print "dist1: ", dist1
        print "dist2: ", dist2
        if dist1 < DEF.MAX_DISTANCE or dist2 < DEF.MAX_DISTANCE:
            line_found = True
            if dist1 < dist2:
                correct_guess = guess1
            else:
                correct_guess = guess2

        if line_found is True:
            cv2.circle(canvas, guess1[1], 10, DEF.GREEN, 3)
            cv2.circle(canvas, guess2[1], 10, DEF.BLUE, 3)
            # cv2.line(canvas, gauge.lines[this_pair[0]][0], gauge.lines[this_pair[0]][1], (255, 0, 0),
            #          DEF.THICKNESS)
            # cv2.line(canvas, gauge.lines[this_pair[1]][0], gauge.lines[this_pair[1]][1], (255, 0, 0),
            #          DEF.THICKNESS)
            break

    if line_found is False:
        EX.error_output(error_screen, err_msg="no positive pair")
        needle_angle = 0.0
    else:
        ref_offset = np.pi
        needle_angle = geometry.rad_to_degrees(correct_guess[0] - ref_offset)
    if needle_angle < 0.0:
        needle_angle += 360
    print "original_guess: ", needle_angle

    angle_adjustment_set = np.arange(needle_angle - 5.0, needle_angle + 5.0,
                                     0.1)
    sum_pixels = []
    for adj_angle in angle_adjustment_set:
        gauge.update_needle(adj_angle)
        gauge.get_needle((circle_x, circle_y - 10))
        overlap = cv2.bitwise_and(thresholded, gauge.needle_canvas)
        sum_pixels.append(np.sum(overlap / 255))
    adjustment_index = sum_pixels.index(max(sum_pixels))
    needle_angle = angle_adjustment_set[adjustment_index]
    gauge.update_needle(needle_angle)
    gauge.get_needle((circle_x, circle_y - 10))
    needle_rgb = cv2.cvtColor(gauge.needle_canvas, cv2.COLOR_GRAY2BGR)
    canvas = cv2.add(canvas, needle_rgb / 2)
    print "updated_guess: ", needle_angle

    pressure = np.interp(
        needle_angle, [DEF.GAUGE_MIN['angle'], DEF.GAUGE_MAX['angle']],
        [DEF.GAUGE_MIN['pressure'], DEF.GAUGE_MAX['pressure']])
    pressure_str = str(round(pressure, 2))
    print "pressure = ", pressure
    display_values(canvas, pressure_str)
    final_line_pt1 = (
        int(circle_x +
            radius * np.sin(geometry.degrees_to_radians(needle_angle))),
        int(circle_y -
            radius * np.cos(geometry.degrees_to_radians(needle_angle))))
    final_line_pt2 = (
        int(circle_x +
            radius * np.sin(geometry.degrees_to_radians(needle_angle + 180))),
        int(circle_y -
            radius * np.cos(geometry.degrees_to_radians(needle_angle + 180))))
    cv2.line(canvas,
             final_line_pt1,
             final_line_pt2,
             DEF.RED,
             thickness=DEF.THICKNESS * 2)
    prep = cv2.bitwise_and(prep, mask)
    # plt.subplot(231)
    # plt.imshow(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
    # plt.subplot(232)
    # plt.imshow(255 - mask_bandpass, 'gray')
    # plt.subplot(233)
    # plt.imshow(prep, 'gray')
    # plt.subplot(234)
    # plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
    # plt.subplot(235)
    # plt.plot(hist0)
    # plt.xlim([0, 256])
    # plt.ylim([0, 50000])
    # plt.show()

    cv2.circle(prep, (circle_x, circle_y), 25, (0, 255, 0), DEF.THICKNESS)
    prep = cv2.cvtColor(prep, cv2.COLOR_GRAY2BGR)
    canvas = np.concatenate((im, canvas), axis=1)
    host.write_to_file('w', "Pressure: " + pressure_str + " MPa",
                       "Temperature: " + str(host.read_temp()))
    host.write_to_file('a', "circle_x:" + str(circle_x),
                       "circle_y:" + str(circle_y), "radius:" + str(radius))
    # pass in tuples ("filename.ext", img_to_write)
    image_path = re.sub('file', str(output_file), DEF.OUTPUT_PATH)
    host.write_image((image_path, canvas))
    image_path = re.sub('file', str(output_file), DEF.THRESH_PATH)
    host.write_image((image_path, thresholded))
コード例 #5
0
ファイル: gm_app.py プロジェクト: naktamello/gauge_monitor
def app(input_file, output_file):
    gauge = process_image.Gauge()
    host = housekeeping.OS()
    # setup image
    reuse_circle = housekeeping.test_time()
    im = host.get_image(DEF.SAMPLE_PATH, input_file)
    gray_im = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
    prep = gray_im  # copy of grayscale image to be preprocessed for angle detection
    mask = np.zeros_like(prep)
    canvas = np.zeros_like(im)  # output image
    np.copyto(canvas, im)
    error_screen = np.zeros_like(im)
    height, width = gray_im.shape
    assert height == DEF.HEIGHT
    assert width == DEF.WIDTH

    # mask_bandpass is a mask of the gauge obtained from histogram peaks
    hist0, mask_bandpass = hist_an.bandpass(gray_im)
    # prep is binary image ready for line detection
    prep = process_image.blur_n_threshold(prep, do_blur=1)
    prep = process_image.erode_n_dilate(prep)
    # host.write_image(("mask_bandpass.jpg", mask_bandpass))
    # Hough transforms
    if reuse_circle:
        print "reusing previous circle"
        circle_x, circle_y, radius = housekeeping.return_circle()
        circle_x = int(circle_x)
        circle_y = int(circle_y)
    else:
        circles = hough_transforms.hough_c(gray_im)
        for c in circles[:1]:
            # scale factor makes applicable area slightly larger just to be safe we are not missing anything
            cv2.circle(mask, (c[0], c[1]), int(c[2] * DEF.CIRCLE_SCALE_FACTOR), (255, 255, 255), -1)
            prep = cv2.bitwise_and(prep, mask)
            cv2.circle(canvas, (c[0], c[1]), c[2], (0, 255, 0), DEF.THICKNESS)
            circle_x = c[0]
            circle_y = c[1]
            radius = int(c[2])

    cv2.circle(mask, (int(circle_x), int(circle_y)), int(radius * DEF.CIRCLE_SCALE_FACTOR), (255, 255, 255), -1)
    cv2.circle(canvas, (int(circle_x), int(circle_y)), radius, (0, 255, 0), DEF.THICKNESS)
    cv2.circle(canvas, (int(circle_x), int(circle_y-12)), 25, (0, 255, 0), DEF.THICKNESS)
    prep = cv2.bitwise_and(prep, mask)

    prep = cv2.bitwise_and(mask_bandpass, prep)
    thresholded = np.copy(prep)
    prep = process_image.find_contour(prep, draw=True)

    lines = hough_transforms.hough_l(prep)

    for line in lines[:10]:
        for (rho, theta) in line:
            # blue for infinite lines (only draw the 2 strongest)
            x0 = np.cos(theta) * rho
            y0 = np.sin(theta) * rho
            pt1 = (int(x0 + (height + width) * (-np.sin(theta))), int(y0 + (height + width) * np.cos(theta)))
            pt2 = (int(x0 - (height + width) * (-np.sin(theta))), int(y0 - (height + width) * np.cos(theta)))
            gauge.lines.append((pt1, pt2))
            gauge.angles_in_radians.append(theta)
            gauge.angles_in_degrees.append(geometry.rad_to_degrees(theta))
            # cv2.line(canvas, pt1, pt2, (255, 255, 0), DEF.THICKNESS / 3)
    # check standard deviation of angles to catch angle wrap-around (359 to 0)
    angle_std = np.std(gauge.angles_in_degrees)
    if angle_std > 50:
        for i in range(len(gauge.angles_in_degrees)):
            if gauge.angles_in_degrees[i] < 20:
                gauge.angles_in_degrees[i] += 180
                gauge.angles_in_radians[i] += np.pi
    angle_index = geometry.get_angles(gauge.angles_in_degrees)
    if len(gauge.lines) < 2:
        EX.error_output(error_screen, err_msg="unable to find needle from lines")
        sys.exit("unable to find needle form lines")

    line_found = False
    for this_pair in angle_index:
        # print "angle1: ", gauge.angles_in_degrees[this_pair[0]]
        # print "angle2: ", gauge.angles_in_degrees[this_pair[1]]
        print this_pair
        line1 = geometry.make_line(gauge.lines[this_pair[0]])
        line2 = geometry.make_line(gauge.lines[this_pair[1]])
        intersecting_pt = geometry.intersection(line1, line2)
        if intersecting_pt is not None:
            print "Intersection detected:", intersecting_pt
            cv2.circle(canvas, intersecting_pt, 10, DEF.RED, 3)
        else:
            print "No single intersection point detected"

        # Although we found a line coincident with the gauge needle,
        # we need to find which of the two direction it is pointing
        # guess1 and guess2 are 180 degrees apart
        avg_theta = (gauge.angles_in_radians[this_pair[0]] + gauge.angles_in_radians[this_pair[1]]) / 2
        guess1 = [avg_theta, (0, 0)]
        guess2 = [avg_theta + np.pi, (0, 0)]
        if guess2[0] > 2 * np.pi:  # in case adding pi made it greater than 2pi
            guess2[0] -= 2 * np.pi
        print "guess1: ", guess1[0]
        print "guess2: ", guess2[0]
        guess1[1] = (int(circle_x + radius * np.sin(guess1[0])), int(circle_y - radius * np.cos(guess1[0])))
        guess2[1] = (int(circle_x + radius * np.sin(guess2[0])), int(circle_y - radius * np.cos(guess2[0])))
        # find the distance between our guess and intersection of gauge needle lines
        # the guess that is closer to the intersection is the correct one
        dist1 = math.hypot(intersecting_pt[0] - guess1[1][0], intersecting_pt[1] - guess1[1][1])
        dist2 = math.hypot(intersecting_pt[0] - guess2[1][0], intersecting_pt[1] - guess2[1][1])
        print "dist1: ", dist1
        print "dist2: ", dist2
        if dist1 < DEF.MAX_DISTANCE or dist2 < DEF.MAX_DISTANCE:
            line_found = True
            if dist1 < dist2:
                correct_guess = guess1
            else:
                correct_guess = guess2

        if line_found is True:
            cv2.circle(canvas, guess1[1], 10, DEF.GREEN, 3)
            cv2.circle(canvas, guess2[1], 10, DEF.BLUE, 3)
            # cv2.line(canvas, gauge.lines[this_pair[0]][0], gauge.lines[this_pair[0]][1], (255, 0, 0),
            #          DEF.THICKNESS)
            # cv2.line(canvas, gauge.lines[this_pair[1]][0], gauge.lines[this_pair[1]][1], (255, 0, 0),
            #          DEF.THICKNESS)
            break

    if line_found is False:
        EX.error_output(error_screen, err_msg="no positive pair")
        needle_angle = 0.0
    else:
        ref_offset = np.pi
        needle_angle = geometry.rad_to_degrees(correct_guess[0] - ref_offset)
    if needle_angle < 0.0:
        needle_angle += 360
    print "original_guess: ", needle_angle

    angle_adjustment_set = np.arange(needle_angle-5.0,needle_angle+5.0,0.1)
    sum_pixels = []
    for adj_angle in angle_adjustment_set:
        gauge.update_needle(adj_angle)
        gauge.get_needle((circle_x, circle_y-10))
        overlap = cv2.bitwise_and(thresholded, gauge.needle_canvas)
        sum_pixels.append(np.sum(overlap/255))
    adjustment_index = sum_pixels.index(max(sum_pixels))
    needle_angle = angle_adjustment_set[adjustment_index]
    gauge.update_needle(needle_angle)
    gauge.get_needle((circle_x, circle_y-10))
    needle_rgb = cv2.cvtColor(gauge.needle_canvas, cv2.COLOR_GRAY2BGR)
    canvas = cv2.add(canvas, needle_rgb/2)
    print "updated_guess: ", needle_angle

    pressure = np.interp(needle_angle, [DEF.GAUGE_MIN['angle'], DEF.GAUGE_MAX['angle']],
                         [DEF.GAUGE_MIN['pressure'], DEF.GAUGE_MAX['pressure']])
    pressure_str = str(round(pressure, 2))
    print "pressure = ", pressure
    display_values(canvas, pressure_str)
    final_line_pt1 = (int(circle_x + radius * np.sin(geometry.degrees_to_radians(needle_angle))), int(circle_y - radius * np.cos(geometry.degrees_to_radians(needle_angle))))
    final_line_pt2 = (int(circle_x + radius * np.sin(geometry.degrees_to_radians(needle_angle + 180))), int(circle_y - radius * np.cos(geometry.degrees_to_radians(needle_angle + 180))))
    cv2.line(canvas, final_line_pt1, final_line_pt2, DEF.RED, thickness=DEF.THICKNESS*2)
    prep = cv2.bitwise_and(prep, mask)
    # plt.subplot(231)
    # plt.imshow(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
    # plt.subplot(232)
    # plt.imshow(255 - mask_bandpass, 'gray')
    # plt.subplot(233)
    # plt.imshow(prep, 'gray')
    # plt.subplot(234)
    # plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
    # plt.subplot(235)
    # plt.plot(hist0)
    # plt.xlim([0, 256])
    # plt.ylim([0, 50000])
    # plt.show()

    cv2.circle(prep, (circle_x, circle_y), 25, (0, 255, 0), DEF.THICKNESS)
    prep = cv2.cvtColor(prep, cv2.COLOR_GRAY2BGR)
    canvas = np.concatenate((im, canvas), axis=1)
    host.write_to_file('w', "Pressure: " + pressure_str + " MPa", "Temperature: " + str(host.read_temp()))
    host.write_to_file('a', "circle_x:" + str(circle_x), "circle_y:" + str(circle_y), "radius:" + str(radius))
    # pass in tuples ("filename.ext", img_to_write)
    image_path = re.sub('file', str(output_file), DEF.OUTPUT_PATH)
    host.write_image((image_path, canvas))
    image_path = re.sub('file', str(output_file), DEF.THRESH_PATH)
    host.write_image((image_path, thresholded))
コード例 #6
0
def visualize(b, theta, bo, ro, res=4999):

    # Find angles of intersection
    kappa, lam, xi, code = get_angles(b, theta, np.cos(theta), np.sin(theta),
                                      bo, ro)
    phi = kappa - np.pi / 2
    print(code)

    # Equation of half-ellipse
    x = np.linspace(-1, 1, 1000)
    y = b * np.sqrt(1 - x**2)
    x_t = x * np.cos(theta) - y * np.sin(theta)
    y_t = x * np.sin(theta) + y * np.cos(theta)

    # Shaded regions
    p = np.linspace(-1, 1, res)
    xpt, ypt = np.meshgrid(p, p)
    cond1 = xpt**2 + (ypt - bo)**2 < ro**2  # inside occultor
    cond2 = xpt**2 + ypt**2 < 1  # inside occulted
    xr = xpt * np.cos(theta) + ypt * np.sin(theta)
    yr = -xpt * np.sin(theta) + ypt * np.cos(theta)
    cond3 = yr > b * np.sqrt(1 - xr**2)  # above terminator
    img_day_occ = np.zeros_like(xpt)
    img_day_occ[cond1 & cond2 & cond3] = 1
    img_night_occ = np.zeros_like(xpt)
    img_night_occ[cond1 & cond2 & ~cond3] = 1
    img_night = np.zeros_like(xpt)
    img_night[~cond1 & cond2 & ~cond3] = 1

    # Plot
    if len(lam):
        fig, ax = plt.subplots(1, 3, figsize=(14, 5))
        fig.subplots_adjust(left=0.025, right=0.975, bottom=0.05, top=0.825)
        ax[0].set_title("T", color="r")
        ax[1].set_title("P", color="r")
        ax[2].set_title("Q", color="r")
    else:
        fig, ax = plt.subplots(1, 2, figsize=(9, 5))
        fig.subplots_adjust(left=0.025, right=0.975, bottom=0.05, top=0.825)
        ax[0].set_title("T", color="r")
        ax[1].set_title("P", color="r")

    # Labels
    for i in range(len(phi)):
        ax[0].annotate(
            r"$\xi_{} = {:.1f}^\circ$".format(i + 1, xi[i] * 180 / np.pi),
            xy=(0, 0),
            xycoords="axes fraction",
            xytext=(5, 25 - i * 20),
            textcoords="offset points",
            fontsize=10,
            color="C0",
        )
        ax[1].annotate(
            r"$\phi_{} = {:.1f}^\circ$".format(i + 1, phi[i] * 180 / np.pi),
            xy=(0, 0),
            xycoords="axes fraction",
            xytext=(5, 25 - i * 20),
            textcoords="offset points",
            fontsize=10,
            color="C0",
        )
    for i in range(len(lam)):
        ax[2].annotate(
            r"$\lambda_{} = {:.1f}^\circ$".format(i + 1, lam[i] * 180 / np.pi),
            xy=(0, 0),
            xycoords="axes fraction",
            xytext=(5, 25 - i * 20),
            textcoords="offset points",
            fontsize=10,
            color="C0",
        )

    # Draw basic shapes
    for axis in ax:
        axis.axis("off")
        axis.add_artist(plt.Circle((0, bo), ro, fill=False))
        axis.add_artist(plt.Circle((0, 0), 1, fill=False))
        axis.plot(x_t, y_t, "k-", lw=1)
        axis.set_xlim(-1.25, 1.25)
        axis.set_ylim(-1.25, 1.25)
        axis.set_aspect(1)
        axis.imshow(
            img_day_occ,
            origin="lower",
            extent=(-1, 1, -1, 1),
            alpha=0.25,
            cmap=LinearSegmentedColormap.from_list("cmap1",
                                                   [(0, 0, 0, 0), "k"], 2),
        )
        axis.imshow(
            img_night_occ,
            origin="lower",
            extent=(-1, 1, -1, 1),
            alpha=0.5,
            cmap=LinearSegmentedColormap.from_list("cmap1",
                                                   [(0, 0, 0, 0), "k"], 2),
        )
        axis.imshow(
            img_night,
            origin="lower",
            extent=(-1, 1, -1, 1),
            alpha=0.75,
            cmap=LinearSegmentedColormap.from_list("cmap1",
                                                   [(0, 0, 0, 0), "k"], 2),
        )

    # Draw integration paths
    if len(phi):
        for k in range(0, len(phi) // 2 + 1, 2):

            # T
            # This is the *actual* angle along the ellipse
            xi_p = np.arctan(np.abs(b) * np.tan(xi))
            xi_p[xi_p < 0] += np.pi
            if np.abs(b) < 1e-4:
                ax[0].plot(
                    [
                        np.cos(xi[k]) * np.cos(theta),
                        np.cos(xi[k + 1]) * np.cos(theta),
                    ],
                    [
                        np.cos(xi[k]) * np.sin(theta),
                        np.cos(xi[k + 1]) * np.sin(theta),
                    ],
                    color="r",
                    lw=2,
                    zorder=3,
                )
            else:
                if xi_p[k] > xi_p[k + 1]:
                    # TODO: CHECK ME
                    xi_p[[k, k + 1]] = xi_p[[k + 1, k]]
                arc = Arc(
                    (0, 0),
                    2,
                    2 * np.abs(b),
                    theta * 180 / np.pi,
                    np.sign(b) * xi_p[k + 1] * 180 / np.pi,
                    np.sign(b) * xi_p[k] * 180 / np.pi,
                    color="r",
                    lw=2,
                    zorder=3,
                )
                ax[0].add_patch(arc)

            # P
            arc = Arc(
                (0, bo),
                2 * ro,
                2 * ro,
                0,
                phi[k] * 180 / np.pi,
                phi[k + 1] * 180 / np.pi,
                color="r",
                lw=2,
                zorder=3,
            )
            ax[1].add_patch(arc)

    if len(lam):

        # Q
        arc = Arc(
            (0, 0),
            2,
            2,
            0,
            lam[0] * 180 / np.pi,
            lam[1] * 180 / np.pi,
            color="r",
            lw=2,
            zorder=3,
        )
        ax[2].add_patch(arc)

    # Draw axes
    ax[0].plot(
        [-np.cos(theta), np.cos(theta)],
        [-np.sin(theta), np.sin(theta)],
        color="k",
        ls="--",
        lw=0.5,
    )
    ax[0].plot([0], [0], "C0o", ms=4, zorder=4)
    ax[1].plot(
        [-ro, ro],
        [bo, bo],
        color="k",
        ls="--",
        lw=0.5,
    )
    ax[1].plot([0], [bo], "C0o", ms=4, zorder=4)
    if len(lam):
        ax[2].plot(
            [-1, 1],
            [0, 0],
            color="k",
            ls="--",
            lw=0.5,
        )
        ax[2].plot(0, 0, "C0o", ms=4, zorder=4)

    # Draw points of intersection & angles
    sz = [0.25, 0.5, 0.75, 1.0]
    for i, xi_i in enumerate(xi):

        # -- T --

        # xi angle
        ax[0].plot(
            [0, np.cos(np.sign(b) * xi_i + theta)],
            [0, np.sin(np.sign(b) * xi_i + theta)],
            color="C0",
            lw=1,
        )

        # tangent line
        x0 = np.cos(xi_i) * np.cos(theta)
        y0 = np.cos(xi_i) * np.sin(theta)
        ax[0].plot(
            [x0, np.cos(np.sign(b) * xi_i + theta)],
            [y0, np.sin(np.sign(b) * xi_i + theta)],
            color="k",
            ls="--",
            lw=0.5,
        )

        # mark the polar angle
        ax[0].plot(
            [np.cos(np.sign(b) * xi_i + theta)],
            [np.sin(np.sign(b) * xi_i + theta)],
            "C0o",
            ms=4,
            zorder=4,
        )

        # draw and label the angle arc
        if np.sin(xi_i) != 0:
            angle = sorted([theta, np.sign(b) * xi_i + theta])
            arc = Arc(
                (0, 0),
                sz[i],
                sz[i],
                0,
                angle[0] * 180 / np.pi,
                angle[1] * 180 / np.pi,
                color="C0",
                lw=0.5,
            )
            ax[0].add_patch(arc)
            ax[0].annotate(
                r"$\xi_{}$".format(i + 1),
                xy=(
                    0.5 * sz[i] * np.cos(0.5 * np.sign(b) * xi_i + theta),
                    0.5 * sz[i] * np.sin(0.5 * np.sign(b) * xi_i + theta),
                ),
                xycoords="data",
                xytext=(
                    7 * np.cos(0.5 * np.sign(b) * xi_i + theta),
                    7 * np.sin(0.5 * np.sign(b) * xi_i + theta),
                ),
                textcoords="offset points",
                ha="center",
                va="center",
                fontsize=8,
                color="C0",
            )

        # points of intersection?
        tol = 1e-7
        for phi_i in phi:
            x_phi = ro * np.cos(phi_i)
            y_phi = bo + ro * np.sin(phi_i)
            x_xi = np.cos(theta) * np.cos(xi_i) - b * np.sin(theta) * np.sin(
                xi_i)
            y_xi = np.sin(theta) * np.cos(xi_i) + b * np.cos(theta) * np.sin(
                xi_i)
            if np.abs(y_phi - y_xi) < tol and np.abs(x_phi - x_xi) < tol:
                ax[0].plot(
                    [ro * np.cos(phi_i)],
                    [bo + ro * np.sin(phi_i)],
                    "C0o",
                    ms=4,
                    zorder=4,
                )

    for i, phi_i in enumerate(phi):

        # -- P --

        # points of intersection
        ax[1].plot(
            [0, ro * np.cos(phi_i)],
            [bo, bo + ro * np.sin(phi_i)],
            color="C0",
            ls="-",
            lw=1,
        )
        ax[1].plot(
            [ro * np.cos(phi_i)],
            [bo + ro * np.sin(phi_i)],
            "C0o",
            ms=4,
            zorder=4,
        )

        # draw and label the angle arc
        angle = sorted([0, phi_i])
        arc = Arc(
            (0, bo),
            sz[i],
            sz[i],
            0,
            angle[0] * 180 / np.pi,
            angle[1] * 180 / np.pi,
            color="C0",
            lw=0.5,
        )
        ax[1].add_patch(arc)
        ax[1].annotate(
            r"${}\phi_{}$".format("-" if phi_i < 0 else "", i + 1),
            xy=(
                0.5 * sz[i] * np.cos(0.5 * (phi_i % (2 * np.pi))),
                bo + 0.5 * sz[i] * np.sin(0.5 * (phi_i % (2 * np.pi))),
            ),
            xycoords="data",
            xytext=(
                7 * np.cos(0.5 * (phi_i % (2 * np.pi))),
                7 * np.sin(0.5 * (phi_i % (2 * np.pi))),
            ),
            textcoords="offset points",
            ha="center",
            va="center",
            fontsize=8,
            color="C0",
            zorder=4,
        )

    for i, lam_i in zip(range(len(lam)), lam):

        # -- Q --

        # points of intersection
        ax[2].plot(
            [0, np.cos(lam_i)],
            [0, np.sin(lam_i)],
            color="C0",
            ls="-",
            lw=1,
        )
        ax[2].plot(
            [np.cos(lam_i)],
            [np.sin(lam_i)],
            "C0o",
            ms=4,
            zorder=4,
        )

        # draw and label the angle arc
        angle = sorted([0, lam_i])
        arc = Arc(
            (0, 0),
            sz[i],
            sz[i],
            0,
            angle[0] * 180 / np.pi,
            angle[1] * 180 / np.pi,
            color="C0",
            lw=0.5,
        )
        ax[2].add_patch(arc)
        ax[2].annotate(
            r"${}\lambda_{}$".format("-" if lam_i < 0 else "", i + 1),
            xy=(
                0.5 * sz[i] * np.cos(0.5 * lam_i),
                0.5 * sz[i] * np.sin(0.5 * lam_i),
            ),
            xycoords="data",
            xytext=(
                7 * np.cos(0.5 * lam_i),
                7 * np.sin(0.5 * lam_i),
            ),
            textcoords="offset points",
            ha="center",
            va="center",
            fontsize=8,
            color="C0",
            zorder=4,
        )

    return fig, ax