From 9953327d30fb7033b0c2cfb5cc82d962f003c555 Mon Sep 17 00:00:00 2001 From: "Michael Pilosov, PhD" Date: Tue, 13 Feb 2024 03:26:37 +0000 Subject: [PATCH] copy over color files, use new plotting --- check.py | 14 +- color_poster.py | 235 +++++++++++++++++++++ makefile | 18 +- scripts/lex_sorted_indices.npy | Bin 0 -> 7720 bytes scripts/place.sh | 63 ++++++ scripts/scatter.py | 148 +++++++++++++ scripts/sortcolor.py | 371 +++++++++++++++++++++++++++++++++ 7 files changed, 844 insertions(+), 5 deletions(-) create mode 100644 color_poster.py create mode 100644 scripts/lex_sorted_indices.npy create mode 100644 scripts/place.sh create mode 100644 scripts/scatter.py create mode 100644 scripts/sortcolor.py diff --git a/check.py b/check.py index 467234a..56b458f 100644 --- a/check.py +++ b/check.py @@ -101,18 +101,24 @@ def plot_preds( ax.set_xticks([]) ax.set_yticks([]) - ax.axis("off") ax.set_aspect("equal") + ax.axis("off") + radius = 1 + ax.set_ylim(-radius, radius) + ax.set_xlim(-radius, radius) + # Overlay white circle - radius = 1 / 3 + inner_radius = 1 / 3 circle = patches.Circle( - (0, 0), radius, transform=ax.transData._b, color="white", zorder=2 + (0, 0), inner_radius, transform=ax.transData._b, color="white", zorder=2 ) ax.add_patch(circle) fig.tight_layout(pad=0) - plt.savefig(f"{fname}.png", dpi=dpi, transparent=False) + plt.savefig( + f"{fname}.png", dpi=dpi, transparent=False, pad_inches=0, bbox_inches="tight" + ) plt.close() diff --git a/color_poster.py b/color_poster.py new file mode 100644 index 0000000..1f40b83 --- /dev/null +++ b/color_poster.py @@ -0,0 +1,235 @@ +from typing import List + +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +import numpy as np + +# set font by default to courier new +plt.rcParams["font.family"] = "PT Mono" +# # sort by the proximity to the colors in viridis +# # Importing necessary functions + +# # Calculate the proximity of each color in XKCD_COLORS to the viridis colormap +# def calculate_proximity_to_viridis(color): +# rgb_triple = mcolors.to_rgb(mcolors.XKCD_COLORS[color]) +# distances = [(sum((a - b)**2 for a, b in zip(rgb_triple, viridis(i))), i) for i in range(256)] +# _, closest_viridis_value = min(distances, key=lambda x: x[0]) # Find the viridis color with the minimum distance +# return closest_viridis_value / 255 # Normalize to range (0, 1) + +# # Calculate the proximity values for each color +# proximity_values = {color: calculate_proximity_to_viridis(color) for color in colors} + +# # Sort the colors based on their proximity values +# sorted_colors = sorted(proximity_values.keys(), key=lambda x: proximity_values[x]) + + +def create_color_calibration_image( + colors, ppi: List[int] = [100], index=0, solid_capstyle="butt", antialiased=True +): + first_color = colors[0] + last_color = colors[-1] + print(f"Processing color range: {first_color} to {last_color}") + # Conversion factor: 1 inch = dpi pixels + vert_space = 1 / 2 # inches + fontsize = 12 # points + + # Figure settings + fig_width = 4.0 # inches + fig_height = len(colors) * vert_space + fig, ax = plt.subplots(figsize=(fig_width, fig_height)) + + # plot a vertical black rectangle between x=3 and x=4 + ax.axvspan(0.25, 0.75, facecolor="black") + # ax.axvspan(-0.325-0.175, -0.125, facecolor="black") + # Loop through each color + if index % 2 == 0: + skip = -1 + else: + skip = 1 + for idx, color in enumerate(colors[::skip]): + # print(color) + y_position = 0.5 - 0.125 + idx * vert_space # Offset each color by 0.3 inches + + # Draw color name + rgb_triple = mcolors.to_rgb(mcolors.XKCD_COLORS[color]) + # round to 4 decimal places + rgb_triple = tuple(round(x, 4) for x in rgb_triple) + # format as string with fixed decimal places + rgb_triple = ", ".join([f"{x:1.4f}" for x in rgb_triple]) + hex_code = mcolors.to_hex(mcolors.XKCD_COLORS[color]) + ax.text( + 1.0, + y_position, + color.replace("xkcd:", ""), + va="center", + fontsize=fontsize, + # bbox=dict(facecolor='gray', alpha=0.21), + ) + ax.text( + 1.25, + y_position - 1.5 * fontsize / 72, + f"{hex_code}\n({rgb_triple})", + va="center", + fontsize=6, + # bbox=dict(facecolor='gray', alpha=0.33), + ) + # ax.text( + # 1.125, + # y_position - 2 * fontsize / 72, + # f"{hex_code}", + # va="center", + # fontsize=fontsize * 0.6, + # ) + + # Draw color square + rect_height = 0.25 + rect_width = 0.25 + square_x_start = 0.75 - rect_width / 2 # Offset from the left + square_y_start = y_position - rect_height # + 0.25 + ax.add_patch( + plt.Rectangle( + (square_x_start, square_y_start), rect_width, rect_height, fc=color + ) + ) + + # Draw lines with varying stroke sizes + line_x_start = 0 # Offset from the left + line_length = 0.5 + line_y_start = y_position - rect_height - 0.075 + line_widths = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2.0, 3.0][::-1] + for width in line_widths: + ax.plot( + [line_x_start, line_x_start + line_length], + [line_y_start, line_y_start], + linewidth=width, + color=color, + antialiased=antialiased, + solid_capstyle=solid_capstyle, + ) + line_y_start += 0.05 + + # # now repeat but vertical of height 1 + # line_x_start = 3.125 - 0.05 # Offset from the left + # line_y_start = y_position + dpi / 960 + # for width in line_widths: + # ax.plot( + # [line_x_start, line_x_start], + # [line_y_start, line_y_start - 0.5], + # linewidth=width, + # color=color, + # antialiased=True, + # ) + # ax.plot( + # [0.5 + line_x_start, 0.5 + line_x_start], + # [line_y_start, line_y_start - 0.5], + # linewidth=width, + # color=color, + # antialiased=True, + # ) + # line_x_start += 0.05 + + # Save the image + # Remove axes + ax.axis("off") + + # ax.set_aspect("equal") + # plt.tight_layout(pad=0) + ax.set_ylim([0, fig_height]) + # ax.set_xlim([0, fig_width]) + ax.set_xlim([0, fig_width]) + # pad = 0.108 + pad = 0 + fig.subplots_adjust(left=0, right=1, top=1, bottom=0) + output_paths = [] + for _dpi in ppi: + _out_path = f"/tmp/color_calibration-{_dpi}_{index:02d}.png" + plt.savefig(_out_path, pad_inches=pad, dpi=_dpi) + output_paths.append(_out_path) + # plt.show() + plt.close() + + return output_paths + + +if __name__ == "__main__": + import argparse + import os + + parser = argparse.ArgumentParser() + parser.add_argument( + "--rows", type=int, default=73, help="Number of entries per column" + ) + parser.add_argument( + "--dir", type=str, default="/Volumes/TMP/tests", help="Directory to save images" + ) + parser.add_argument( + "-k", + "--kind", + type=str, + nargs="+", + default=["hsv", "lex", "lab", "umap"], + help="Kinds of sorting", + ) + parser.add_argument( + "--ppi", action="append", type=int, default=[300], help="Pixels per inch" + ) + parser.add_argument("--aliased", action="store_true", help="Disable antialiasing") + parser.add_argument( + "--capstyle", type=str, default="butt", help="Capstyle of lines" + ) + args = parser.parse_args() + COLUMN_LENGTH = args.rows + KINDS = args.kind + PPIS = args.ppi + DIR = args.dir + ANTIALIASED = not args.aliased + CAPSTYLE = args.capstyle + + # COLUMN_LENGTH = 73 # results in 13 unfiltered columns (perfect) + # COLUMN_LENGTH = ( + # 106 # results in 9 unfiltered columns (last one short), square-ish image + # ) + OMITTED_COLORS = [ + # "black", + # "white", + # "poop", + # "poo brown", + # "shit", + # "shit brown", + ] + # OMITTED_COLORS = list(map(lambda s: f"xkcd:{s}", OMITTED_COLORS)) + # KIND = "hsv" # choose from umap, hsv + for KIND in KINDS: + colors = list(mcolors.XKCD_COLORS.keys()) + sorted_indices = np.load(f"scripts/{KIND}_sorted_indices.npy") + sorted_colors = [colors[idx] for idx in sorted_indices] + + colors = sorted_colors + colors = [c for c in colors if c not in OMITTED_COLORS] + print(f"Total number of colors: {len(colors)}") + + chunks = [ + colors[i : i + COLUMN_LENGTH] for i in range(0, len(colors), COLUMN_LENGTH) + ] + + for idx, color_part in enumerate(chunks): + image_path = create_color_calibration_image( + colors=color_part, + ppi=PPIS, + index=idx, + antialiased=ANTIALIASED, + solid_capstyle=CAPSTYLE, + ) + os.system(f"identify {image_path[0]}") + + for PPI in PPIS: + # use imagemagick to stitch together the images horizontally + os.system( + f"convert +append /tmp/color_calibration-{PPI}_*.png /tmp/color_calibration-{PPI}.png" + ) + os.system(f"rm /tmp/color_calibration-{PPI}_*") + print(f"Final image saved to /tmp/color_calibration-{PPI}.png") + os.system( + f"mkdir -p {DIR} && cp /tmp/color_calibration-{PPI}.png {DIR}/xkcd_{COLUMN_LENGTH}_{KIND}_{PPI}.png" + ) + print(f"Copied to {DIR}/xkcd_{COLUMN_LENGTH}_{KIND}_{PPI}.png") diff --git a/makefile b/makefile index 780943e..c382803 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ lint: black . isort --profile=black *.py - flake8 --ignore E501,W503 *.py + flake8 --ignore E501,W503,E203 *.py test: # python main.py --alpha 1 --lr 1e-2 --max_epochs 200 --bs 256 --seed 856 --width 2048 @@ -42,12 +42,28 @@ search: lint hsv: python hsv.py +# TODO: replace this with what we used in day-in-the-life animate: ffmpeg -i lightning_logs/version_258/e%04d.png \ -c:v libx264 \ -vf "fps=12,format=yuv420p,pad=ceil(iw/2)*2:ceil(ih/2)*2" \ ~/animated.mp4 + +umap: + for seed in `seq 0 100`; do \ + python scripts/sortcolor.py -s umap --seed $$seed --dpi 100 --size 6 ; \ + done + + +lex: + python scripts/sortcolor.py -s lex --dpi 300 --size 6 --radius 0.5 + +arrange: + python scripts/sortcolor.py -s hsv --dpi 100 --size 3 && \ + sh scripts/place.sh + + clean: rm -rf lightning_logs rm -rf .lr_find_*.ckpt diff --git a/scripts/lex_sorted_indices.npy b/scripts/lex_sorted_indices.npy new file mode 100644 index 0000000000000000000000000000000000000000..971bb7af3caa86f1e053f2ba4cf65bc7bf71248f GIT binary patch literal 7720 zcmbWyWnVS)emU+m$L1OEMsibwvF0pH_?Py6?;uzUjD2-8LR zFCQ;oMn}h|{3JM%-x?RVb5eevUP)J^EBl_Tc7rg9{EdEb`XV;QM)s|+Fs73a#rRm? zy`*$BdLEXwpXWP{>E**&{B(MU`P=E3^4*w>-?O}d|KPn+5#P`6OTUzlp|8mo(SOQgxEn&3!$SA~ zv+8A`htQqf8-m#|rS~4`&(be}6>uXq){jru!JY1f(uv&5MIWK#;FtUtm>B8gro(*aC>$;CXjhVdnV%AC%OB$g_v+9G^ipFe|2%z*o{Jgz zIq0SCt*4{V!|)D1#K#y%f2@0re19?eH8#=fBHv@5LOzLpNAJdFc3;`A`Rx^!`RIsG+#6|dQ6@ZK$YrG6E9w|+PN zb-Es=;CIm<&#z9$rPt`CqFd2fy}y>fpN?gpRBs9WMBaujEbl}g#6EiCu@V0Z`ZPwi zpCE5fivzau7vmIpcZJ3DQ2bnPrr)y|zvur&{jR?J6Sxa&;c5HUbZh%g{Izs* z`9?aJhJf|{iG@^ySNV1T?o z|2BSw$FVD(!%BAX=y8|{^UKFzXWvzpzJ*KqdvJh#d--twT>erYo&-1PMb&Fd*O3pz zb^PM)Y}VgEx6sRpmGyV%#pVykIr2Sp6FNRU!`(@k#+^v=sdQCbiACh4=!Ep2?)=LC zfe!QT68uJ<$L>phHasTZLT}J(gA?5?Lm$*T%#Uv$lRtt^Ll4GW_NDFKV>d*eyF*BFQy z-Fc0X^?S;5_}*{%o#>qM=D6HGJC4))ie5)I#IyW1_C@KFn8@A z&resvPx%wwSwpXNr##l)$R*<0y+V`S?`p0;$k(u(t2V1 zw73cv+AqPp_{{!_-cdRjoA7gEef~l372*%#e~bm(??uV&%sdnUf(gA|CmlgZ?PYu zA5J%*`{) z{#Ez;^Pl2&{#LxNpOT(|!@U2syf(jvJJaR!`6KkY%CGPj<6N91{}O-VKcN%3yOC~8 z*T5gWGtTaiUM9M(JUvdqdiDeKn@+eZ-~?Q-Rvsp_rS^W zsrudczv*Yjs`6L3%kCom#_n@WCI6NF8hc_jc@QS_&N;l`Zf*RI--_;Uw~5~nkGuaH zZqfgnzN{B$Uy%MrUQxfDyej6F_n;T*{e?fu-@CV0FFD4Q57R$`i}=6T#ip~%^J7DP zT5Qh`vOB?Fzz_CrO@12wbv)0{Zl908n*SqzI<~+kdM)Ww7z1~B_oZGTx+Gl+Ti|)S zrF04XLi|to_wX*>kT1u{m{|V8?9klA-{2IiggNcn(;dCD!n+swweY$8BY8}IUfjlSr?)zy z@0|{MEBJ%yXm-8n;rexPG`}t;!5#9+?k(i6cCW8|6lTV|dI9thx)|Pcub(^@ZpQiY zP7yx+*1ilKfP?r=>F@0h(fQ;@={)jRxJ_Pw{)sL~C&smY_egif=tYv}#ZUO{=oxfk z`_uNdjj?Yqij+eN0U>HP!0VBX{a literal 0 HcmV?d00001 diff --git a/scripts/place.sh b/scripts/place.sh new file mode 100644 index 0000000..64fe0bc --- /dev/null +++ b/scripts/place.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Constants +# SEED=20230920 +SEED=0 +# INPUT_FILE="${DIR}/arrangement_$SEED.txt" +DIR=/teamspace/studios/this_studio/out_sortcolors +INPUT_FILE="${DIR}/arrangement_grid.txt" +# OUTPUT_IMAGE="${DIR}/circle_composite_$SEED.png" +TYPE=circle +OUTPUT_IMAGE="${DIR}/${TYPE}_composite_grid.png" +DPI=100 +CANVAS_SIZE=$((72*$DPI)) # 72 inches +CIRCLE_IMAGE="${DIR}/hsv_sorted_colors_circle.png" + +identify $CIRCLE_IMAGE + +# PREP +echo "Building ops" +# Build the composite operations string +composite_ops="" +idx=1 +while IFS=, read -r x y; do + # Translate so that (0,0) becomes the center of the canvas + fx=$(echo "$x*$DPI + $CANVAS_SIZE/2" | bc -l) + fy=$(echo "$CANVAS_SIZE/2 - $y*$DPI" | bc -l) + + # Convert the final float values to integers + ix=$(printf "%.0f" "$fx") + iy=$(printf "%.0f" "$fy") + if [[ idx -eq 42 ]]; then + CIRCLE_IMAGE="${DIR}/hsv_sorted_colors_${TYPE}.png" + else + idx_str=$(printf "%02d" "$idx") + CIRCLE_IMAGE="${DIR}/${idx_str}umap_sorted_colors_${TYPE}.png" + # CIRCLE_IMAGE="${DIR}/hsv_sorted_colors_${TYPE}.png" + fi + + # Add to the composite operations string + composite_ops="$composite_ops \( $CIRCLE_IMAGE \) -compose Over -geometry +$ix+$iy -composite" + idx=$((idx+1)) + +done < $INPUT_FILE + +# COMPOSITE +echo "Compositing" +# Use convert with the built composite operations string +eval "convert -units PixelsPerInch \ + -size ${CANVAS_SIZE}x${CANVAS_SIZE} xc:white \ + $composite_ops \ + -density $DPI \ + $OUTPUT_IMAGE" + +echo "Saved $OUTPUT_IMAGE" + +# DEBUG +# eval "convert -units PixelsPerInch \ +# $OUTPUT_IMAGE \ +# \( ${DIR}/arrangement_$SEED.png -evaluate Multiply 0.5 \) \ +# -gravity center -composite \ +# -density $DPI \ +# /tmp/debug.png" +# open /tmp/debug.png \ No newline at end of file diff --git a/scripts/scatter.py b/scripts/scatter.py new file mode 100644 index 0000000..337645e --- /dev/null +++ b/scripts/scatter.py @@ -0,0 +1,148 @@ +import matplotlib.patches as patches +import matplotlib.pyplot as plt +import numpy as np +from pathlib import Path + + +def get_quadrant(x, y): + """Return the quadrant of a given point.""" + if x >= 0 and y >= 0: + return 1 + elif x < 0 and y >= 0: + return 2 + elif x < 0 and y < 0: + return 3 + else: + return 4 + + +def is_overlapping(x, y, existing_points, radius): + """Check if a point overlaps with existing points, crosses a quadrant or goes beyond the quadrant lines.""" + current_quadrant = get_quadrant(x, y) + buffer = 0.25 + # Check if circle touches/crosses the x=0 or y=0 lines. + if ( + abs(x) + radius > buffer + and abs(x) - radius < buffer + or abs(y) + radius > buffer + and abs(y) - radius < buffer + ): + return True + + for ex, ey in existing_points: + # Check overlap with existing points + if np.sqrt((x - ex) ** 2 + (y - ey) ** 2) <= 2 * radius + 0.5: + return True + # Check if the circle touches another quadrant + if ( + get_quadrant(ex, ey) != current_quadrant + and np.sqrt((x - ex) ** 2 + (y - ey) ** 2) <= radius + ): + return True + return False + + +DIR = "/teamspace/studios/this_studio/out_sortcolors" + +N = 100 +DPI = 300 +SIZE = 72 # canvas size? +radius = 3 +variance = 72 # Adjust variance as needed + +Path(DIR).mkdir(exist_ok=True, parents=True) + + +with open(f"{DIR}/arrangement_grid.txt", "w") as f: + radius = 3 + half = SIZE / 2 - 4 + # make points an equispaced grid of 10 x 10 ranging from [-35, 35] + interval = (half - (-half)) / 9 # 10 points, so 9 intervals + + # Generate the grid points + points = [ + (x, y) + for x in np.arange(-half, half + 0.1, interval) + for y in np.arange(-half, half + 0.1, interval) + ] + + for x, y in points: + f.write(f"{x-radius}, {y+radius}\n") + # f.write(f"{x}, {y}\n") + +print("wrote grid") + + +for seed in range(11, 22): + try: + np.random.seed(seed) + + # To store plotted points + points = [] + max_iterations = int(1e7) + iterations = 0 + # Generate points + for k in range(N): + while True: + # x, y = np.random.normal(0, variance), np.random.normal(0, variance) + random_angle = np.random.uniform(0, 2 * np.pi) + # random_radius = np.random.uniform(0.25+radius, SIZE/2 - radius - 0.25) + random_radius = abs(np.random.normal(0, variance)) + x = random_radius * np.cos(random_angle) + y = random_radius * np.sin(random_angle) + iterations += 1 + if not is_overlapping(x, y, points, radius): + if max(abs(x), abs(y)) + radius < SIZE / 2 - 0.25: + points.append((x, y)) + break + if iterations > max_iterations: + raise ValueError(f"Too many iterations: {k} points") + print(f"{k}: ({x}, {y}) @ {iterations:09d}") + + # Create plot with circles + fig, ax = plt.subplots(1, 1, figsize=(SIZE, SIZE)) + for x, y in points: + circle = patches.Circle((x, y), radius, color="black") + ax.add_patch(circle) + + # Draw the standard quadrants + ax.axhline(0, color="black", linewidth=0.5) + ax.axvline(0, color="black", linewidth=0.5) + + # Use square axis and set limits + + lim_x = ( + max( + abs(max(points, key=lambda t: t[0])[0]), + abs(min(points, key=lambda t: t[0])[0]), + ) + + radius + ) + lim_y = ( + max( + abs(max(points, key=lambda t: t[1])[1]), + abs(min(points, key=lambda t: t[1])[1]), + ) + + radius + ) + lim_x = lim_y = SIZE / 2 + ax.axis("off") + ax.set_aspect("equal") + ax.set_xlim(-lim_x, lim_x) + ax.set_ylim(-lim_y, lim_y) + + # Save and show + fig.tight_layout(pad=0) + plt.savefig( + f"{DIR}/arrangement_{seed}.png", dpi=DPI, bbox_inches="tight", pad_inches=0 + ) + # plt.show() + # also save x/y coords as text file + with open(f"{DIR}/arrangement_{seed}.txt", "w") as f: + for x, y in points: + f.write(f"{x-radius}, {y+radius}\n") + # f.write(f"{x}, {y}\n") + except ValueError as e: + print(f"{seed}: {e}") + except AssertionError as e: + print(f"{seed}: {e}") diff --git a/scripts/sortcolor.py b/scripts/sortcolor.py new file mode 100644 index 0000000..f15d460 --- /dev/null +++ b/scripts/sortcolor.py @@ -0,0 +1,371 @@ +import argparse +from pathlib import Path + +import matplotlib.colors as mcolors +import matplotlib.patches as patches +import matplotlib.pyplot as plt +import numpy as np +from hilbertcurve.hilbertcurve import HilbertCurve + +# Extract XKCD colors +colors = list(mcolors.XKCD_COLORS.keys()) +rgb_values = [mcolors.to_rgb(mcolors.XKCD_COLORS[color]) for color in colors] + +# Parse command-line arguments +parser = argparse.ArgumentParser() +parser.add_argument("-s", "--sort-by", type=str, default="hsv", help="kind of sorting") +parser.add_argument("--seed", type=int, default=21, help="seed for UMAP") +parser.add_argument("--dpi", type=int, default=100, help="dpi for saving") +parser.add_argument("--size", type=float, default=6.0, help="size of figure") +parser.add_argument( + "--fontsize", + type=float, + default=0, + help="fontsize of annotation (default: 0 = None)", +) +parser.add_argument( + "--radius", type=float, default=1 / 3, help="inner radius of circle" +) +args = parser.parse_args() +KIND = args.sort_by +SEED = args.seed +DPI = args.dpi +SIZE = args.size +FONTSIZE = args.fontsize +INNER_RADIUS = args.radius +DIR = "/teamspace/studios/this_studio/out_sortcolors" + +Path(DIR).mkdir(exist_ok=True, parents=True) + + +def peano_curve(n): + """ + Generate Peano curve coordinates for a given order `n`. + """ + if n == 0: + return [(0, 0)] + + prev_curve = peano_curve(n - 1) + max_coord = 3 ** (n - 1) + + # Define the transformations for the Peano curve's 9 segments + transforms = [ + lambda x, y: (x, y), # Bottom-left square + lambda x, y: (x + max_coord, y), # Bottom-middle square + lambda x, y: (x + 2 * max_coord, y), # Bottom-right square + lambda x, y: (x + 2 * max_coord, y + max_coord), # Middle-right square + lambda x, y: ( + x + max_coord, + y + max_coord, + ), # Center square (traversed in reverse) + lambda x, y: (x, y + max_coord), # Middle-left square + lambda x, y: (x, y + 2 * max_coord), # Top-left square + lambda x, y: (x + max_coord, y + 2 * max_coord), # Top-middle square + lambda x, y: (x + 2 * max_coord, y + 2 * max_coord), # Top-right square + ] + + curve = [] + for transform in transforms: + segment = [transform(x, y) for x, y in prev_curve] + # Reverse the traversal for the center square + if transform == transforms[4]: + segment = segment[::-1] + curve += segment + + return curve + + +if KIND in ("lex", "alpha", "abc"): + preds = np.array(colors) + +elif KIND == "umap": + import umap + + # Use UMAP to create a 1D representation + reducer = umap.UMAP( + n_components=1, + n_neighbors=250, + min_dist=0.005, + metric="cosine", + random_state=SEED, + ) + embedding = reducer.fit_transform(np.array(rgb_values)) + + # Sort colors by the 1D representation + preds = embedding[:, 0] + +elif KIND in ("cielab", "lab", "ciede2000"): + from skimage.color import deltaE_ciede2000, rgb2lab + + # CIELAB + # Convert RGB values to CIELAB + lab_values = rgb2lab([rgb_values]) + + # Reference color for sorting (can be the first color or any other reference point) + reference_color = lab_values[0] + + # Compute CIEDE2000 distances of all colors to the reference color + distances = [deltaE_ciede2000(reference_color, color) for color in lab_values] + + # Sort colors by their CIEDE2000 distance to the reference color + # preds = distances).flatten() # awful + lab_values_flat = lab_values.reshape(-1, 3) + # Sort colors based on the L* value in the CIELAB space + # 0 corresponds to the L* channel + preds = lab_values_flat[:, 0] + +elif KIND == "hsv": + from matplotlib.colors import rgb_to_hsv + + # Convert RGB values to HSV + hsv_values = np.array([rgb_to_hsv(np.array(rgb)) for rgb in rgb_values]) + + # Sort colors based on the hue value + # 0 corresponds to the hue component + preds = hsv_values[:, 0] +else: + raise ValueError(f"Unknown kind: {KIND}") + +sorted_indices = np.argsort(preds) + +# Save the sorted indices to disk +if (KIND == "umap" and SEED == 21) or (KIND != "umap"): + file_path = f"scripts/{KIND}_sorted_indices.npy" + np.save(file_path, sorted_indices) + print(f"Sorted indices saved to {file_path}") + +# Sort colors by the 1D representation +sorted_colors = [colors[i] for i in sorted_indices] + +# # Display the sorted colors around the ring of a circle +# # Create a new figure for the circle visualization +# fig, ax = plt.subplots(figsize=(SIZE, SIZE)) + +# # Circle parameters +# center = (0, 0) +# radius = 1.0 + +# # Angle increment (in radians) based on the number of colors +# angle_increment = 2 * np.pi / len(sorted_colors) +# # roll the colors so that the first color is white +# reordered_sorted_colors = sorted_colors.copy() +# white_index = reordered_sorted_colors.index("xkcd:white") +# reordered_sorted_colors = reordered_sorted_colors[white_index:] + reordered_sorted_colors[:white_index] +# # Plot each color around the circle +# for i, color in enumerate(reordered_sorted_colors): +# # Compute start and end angles for the segment +# start_angle = i * angle_increment +# end_angle = (i + 1) * angle_increment + +# # Create a wedge (segment of the circle) for each color +# wedge = patches.Wedge( +# center, radius, 90 + np.degrees(start_angle), 90 + np.degrees(end_angle), fc=color +# ) + +# ax.add_patch(wedge) + +# # Overlay a white circle in the center +# inner_circle = patches.Circle(center, INNER_RADIUS, color="white") +# ax.add_patch(inner_circle) +# if INNER_RADIUS > 0.0: +# fcolor = "black" +# else: +# fcolor = "white" + +# if FONTSIZE > 0.0: +# ax.annotate(f"{KIND.upper()}", center, ha="center", va="center", size=FONTSIZE, color=fcolor) + +# # Set equal scaling and remove axis +# ax.set_aspect("equal") +# ax.axis("off") +# ax.set_ylim(-radius, radius) +# ax.set_xlim(-radius, radius) + +# # Save and display the circle visualization +# prefix = "" +# if KIND == "umap": +# prefix = f"{SEED:02d}" + +# fname = f"{DIR}/{prefix}{KIND}_sorted_colors_circle.png" +# fig.tight_layout(pad=0) +# fig.savefig( +# fname, +# dpi=DPI, +# transparent=True, +# pad_inches=0, +# bbox_inches="tight", +# ) +# print(f"Saved {fname}") +# # plt.show() + + +def plot_preds( + preds, + rgb_values, + fname: str, + roll: bool = False, + dpi: int = 150, + inner_radius: float = 1 / 3, + figsize=(3, 3), +): + sorted_inds = np.argsort(preds.ravel()) + colors = rgb_values[sorted_inds, :3] + if roll: + # find white in colors, put it first. + white = np.array([1, 1, 1]) + white_idx = np.where((colors == white).all(axis=1)) + if white_idx: + white_idx = white_idx[0][0] + colors = np.roll(colors, -white_idx, axis=0) + else: + print("no white, skipping") + # print(white_idx, colors[:2]) + + N = len(colors) + # Create a plot with these hues in a circle + fig, ax = plt.subplots(figsize=figsize, subplot_kw=dict(polar=True)) + + # Each wedge in the circle + theta = np.linspace(0, 2 * np.pi, N, endpoint=False) + np.pi / 2 + width = 2 * np.pi / (N) # equal size for each wedge + + for i in range(N): + ax.bar( + # 2 * np.pi * preds[i], + theta[i], + height=1, + width=width, + edgecolor=colors[i], + linewidth=0.25, + # facecolor=[rgb_values[i][1]]*3, + # facecolor=rgb_values[i], + facecolor=colors[i], + bottom=0.0, + zorder=1, + alpha=1, + align="edge", + ) + + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_aspect("equal") + ax.axis("off") + radius = 1 + ax.set_ylim(-radius, radius) + ax.set_xlim(-radius, radius) + + # Overlay white circle + circle = patches.Circle( + (0, 0), inner_radius, transform=ax.transData._b, color="white", zorder=2 + ) + ax.add_patch(circle) + + if FONTSIZE > 0.0: + ax.annotate( + f"{KIND.upper()}", + center, + ha="center", + va="center", + size=FONTSIZE, + color=fcolor, + ) + + fig.tight_layout(pad=0) + + plt.savefig(fname, dpi=dpi, transparent=False, pad_inches=0, bbox_inches="tight") + plt.close() + + +prefix = "" +if KIND == "umap": + prefix = f"{SEED:02d}" +fname = f"{DIR}/{prefix}{KIND}_sorted_colors_circle.png" + +plot_preds( + preds, + np.array(rgb_values), + fname, + roll=True, + dpi=DPI, + inner_radius=INNER_RADIUS, + figsize=(SIZE, SIZE), +) + +HILBERT = False + +if HILBERT: + # Create Hilbert curve + # We'll set the order such that the number of positions is greater than or equal to the number of colors + hilbert_order = int(np.ceil(0.5 * np.log2(len(sorted_colors)))) + hilbert_curve = HilbertCurve(hilbert_order, 2) + + # Create an image for visualization + image_size = 2**hilbert_order + image = np.ones((image_size, image_size, 3)) + + for i, color in enumerate(sorted_colors): + # Convert linear index to Hilbert coordinates + coords = hilbert_curve.point_from_distance(i) + image[coords[1], coords[0]] = mcolors.to_rgb(color) + + # annotation in upper right + # Display the image + fig, ax = plt.subplots(1, 1, figsize=(SIZE, SIZE)) + ax.imshow(image) + ax.annotate( + f"{KIND.upper()}", + (1.0, 1.0), + ha="right", + va="top", + size=FONTSIZE, + xycoords="axes fraction", + ) + ax.axis("off") + ax.set_aspect("equal") + fig.tight_layout() + fname = f"{DIR}/{prefix}{KIND}_sorted_colors_hilbert.png" + fig.savefig( + fname, + dpi=DPI, + transparent=True, + # bbox_inches="tight", + # pad_inches=0 + ) + print(f"Saved {fname}") + +# plt.show() + + +# # Create peano curve +# order = 0 +# while (3**order)**2 < len(sorted_colors): +# order += 1 + +# # Generate peano curve coordinates for the determined order +# curve_coords = peano_curve(order) +# unique_coords = set(curve_coords) +# print(f"Total coordinates: {len(curve_coords)}") +# print(f"Unique coordinates: {len(unique_coords)}") + +# # If there are more points on the curve than colors, truncate the curve +# if len(curve_coords) > len(sorted_colors): +# print(f"Will be missing {len(curve_coords) - len(sorted_colors)} pixels, percentage: {100 * (len(curve_coords) - len(sorted_colors)) / len(curve_coords)}") +# curve_coords = curve_coords[:len(sorted_colors)] +# if len(curve_coords) < len(sorted_colors): +# raise ValueError("Not enough curve coordinates for the number of colors") + +# # Create an image for visualization +# image_size = (3**order) +# image = np.ones((image_size, image_size, 3)) + +# for i, color in enumerate(sorted_colors): +# # Get the Moore curve coordinates for the current index +# coords = curve_coords[i] +# image[coords[1], coords[0]] = mcolors.to_rgb(color) + +# # Display the image +# plt.figure(figsize=(8, 8)) +# plt.imshow(image) +# plt.axis("off") +# plt.savefig(f"{DIR}/{KIND}_sorted_colors_moore.png", dpi=DPI) +# plt.show()