Categories: supernote

RSS - Atom - Subscribe via email

Recoloring my sketches with Python

Posted: - Modified: | supernote, drawing

[2024-09-29 Sun]: Fix rgb in recolor.py

The SuperNote lets me draw with black, dark gray (0x9d), gray (0xc9), or white. I wanted to make it easy to recolor them, since a little splash of colour makes sketches more fun and also makes them easier to pick out from thumbnails. Here's the Python script I wrote:

Download recolor.py

#!/usr/bin/python3
# Recolor PNGs
#
# (c) 2022 Sacha Chua (sacha@sachachua.com) - MIT License
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
import os
import csv
import argparse
from PIL import Image

DARK_GRAY = 0x9d
GRAY = 0xc9
HEADER_GRAY = 0xca
WHITE = 0xfe

color_dict = {}

def color_to_tuple(color_dict, s):
    if s in color_dict:
        s = color_dict[s]
    s = s.lstrip('#')
    if (s == '.'):
        return (None, None, None)
    elif (len(s) == 2):
        return (int(s, 16), int(s, 16), int(s, 16))
    else:
        return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))

def load_color_dict(filename):
    dict = {}
    with open(os.path.expanduser(filename), newline='') as csvfile:
        reader = csv.reader(csvfile, delimiter=',', quotechar='"')
        for row in reader:
            dict[row[0]] = row[1]
    return dict

def remove_grid(input):
    if isinstance(input, str):
        im = Image.open(input).convert('RGB')
    else:
        im = input
    data = np.array(im)
    freq = get_colors_by_freq(input)
    print(freq)
    return Image.fromarray(data)

def map_colors(input, color_map):
    if isinstance(input, str):
        im = Image.open(input).convert('RGB')
    else:
        im = input
    data = np.array(im)
    red, green, blue = data[:, :, 0], data[:, :, 1], data[:, :, 2]
    for from_c, to_c in color_map.items():
        from_r, from_g, from_b = color_to_tuple(color_dict, from_c)
        to_r, to_g, to_b = color_to_tuple(color_dict, to_c)
        mask = (red == from_r) & (green == from_g) & (blue == from_b)
        data[:, :, :3][mask] = [to_r, to_g, to_b]
    return Image.fromarray(data)

def set_colors_by_freq(input, color_list):
    if isinstance(input, str):
        im = Image.open(input).convert('RGB')
    else:
        im = input
    data = np.array(im)
    red, green, blue = data[:, :, 0], data[:, :, 1], data[:, :, 2]
    sorted_colors = get_colors_by_freq(input)
    freq = iter(color_list.split(','))
    for i, f in enumerate(freq):
        if f != '.':
            to_r, to_g, to_b = color_to_tuple(color_dict, f)
            by_freq = sorted_colors[i][1]
            if isinstance(by_freq, np.uint8):
                mask = (red == by_freq) & (green == by_freq) & (blue == by_freq)
            else:
                mask = (red == by_freq[0]) & (green == by_freq[1]) & (blue == by_freq[2])
            data[:, :, :3][mask] = [to_r, to_b, to_g]
    return Image.fromarray(data)

def color_string_to_map(s):
    color_map = {}
    colors = iter(args.colors.split(','))
    for from_c in colors:
        to_c = next(colors)
        color_map[from_c] = to_c
    return color_map

def get_colors_by_freq(input):
    if isinstance(input, str):
        im = Image.open(input).convert('RGB')
    else:
        im = input
    colors = im.getcolors(im.size[0] * im.size[1])
    return sorted(colors, key=lambda x: x[0], reverse=True)


def print_colors(input):
    sorted_colors = get_colors_by_freq(input)
    for x in sorted_colors:
        if x[0] > 10:
            if isinstance(x[1], np.uint8):
                print('%02x %d' % (x[1], x[0]))
            else:
                print(''.join(['%02x' % c for c in x[1]]) + ' %d' % x[0])


def process_file(input):
    print(input)
    if args.preview:
        output = None
    else:
        output = args.output if args.output else input
        if os.path.isdir(output):
            output = os.path.join(output, os.path.basename(input))
    im = Image.open(input).convert('RGB')
    if args.colors:
        im = map_colors(im, color_string_to_map(args.colors))
    elif args.freq:
        im = set_colors_by_freq(im, args.freq)
    else:
        print_colors(im)
        exit(0)
    if args.preview:
        im.thumbnail((700, 700))
        im.show()
    elif output:
        im.save(output)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Recolor a PNG.',
        formatter_class=argparse.RawTextHelpFormatter,
        epilog="If neither --colors nor --freq are specified, "
        + "display the most frequent colours in the image.")
    parser.add_argument('--colors', help="""Comma-separated list of RGB hex values in the form of old,new,old,new
    Examples:
    9d,ffaaaa,c9,ffd2d2 - reddish
    c9,ffea96 - yellow highlighter
    c9,d2d2ff - light blue
    """)
    parser.add_argument('--freq', help="Color replacements in order of descending frequency (ex: .,ffea96). .: use original color")
    parser.add_argument('--csv', help="CSV of color names to use in the form of colorname,hex")
    parser.add_argument('--preview', help="Preview only", action='store_const', const=True)
    parser.add_argument('input', nargs="+", help="Input file")
    parser.add_argument('--output', help="Output file. If not specified, overwrite input file.")

    args = parser.parse_args()
    color_dict = load_color_dict(args.csv) if args.csv else {}
    for input in args.input:
        process_file(os.path.join(os.getcwd(), input))

I don't think in hex colours, so I added a way to refer to colours by names. I converted this list of Copic CSS colours to a CSV by copying the text, pasting it into a file, and doing a little replacement. It's not complete, but I can copy selected colours from this longer list. I can also add my own. The CSV looks a little like this:

lightgreen,cfe8d3
lightyellow,f6f396
lightblue,b3e3f1
y02,f6f396
w2,ddddd5
b02,b3e3f1
...

It doesn't do any fuzzing or clustering of similar colours, so it won't work well on antialiased images. For the simple sketches I make with the SuperNote, though, it seems to work well enough.

I can preview my changes with something like ./recolor.py ~/sketches/"2022-08-02-01 Playing with my drawing workflow #supernote #drawing #workflow #sketching #kaizen.png" --csv colors.csv --freq .,lightyellow --preview , and then I can take the --preview flag off to overwrite the PNG.

Here's what the output looks like:

View org source for this post

One month with the SuperNote A5X

| supernote, drawing

I've had my SuperNote A5X for a month now, and I really like it.

Text from my sketch

I use it for:

  • untangling thoughts
  • sketchnoting books
  • planning
  • drafting blog posts
  • drawing

A- uses it for: (she's 6 years old)

  • practising cursive
  • doing mazes and dot-to-dots
  • drawing
  • reading lyrics

Things I'm learning:

  • Exporting PNGs at 200% works well for my workflow. I rename them in Dropbox and upload them to sketches.sachachua.com.
  • Carefully copying & deleting pages lets me preserve page numbers. I use lassoed titles for active thoughts and maintain a manual index for other things.
  • Layouts:
    • Landscape: only easier to review on my laptop
    • Portrait columns: lots of scrolling up and down
    • Portrait rows: a little harder to plan, but easier to review
  • Many books fit into one page each.
  • Google Lens does a decent job of converting my handwriting to text (print or cursive, even with a background). Dropbox → Google Photos → Orgzly → Org
  • Draft blog posts go into new notebooks so that I can delete them once converted.
  • The Super Note helps me reclaim a lot of the time I spend waiting for A-. A digital notebook is really nice. Easy to erase, rearrange, export… It works well for me.
  • Part of my everyday carry kit

Ideas for growth:

  • Settle into monthly pages, bullet journaling techniques
  • Practise drawing; use larger graphic elements & organizers, different shades
  • Integrate into Zettelkasten

I put my visual book notes and visual library notes into a Dropbox shared folder so that you can check them out if you have a Supernote. If you don't have a Supernote, you can find my visual book notes at sketches.sachachua.com. Enjoy!

Writing my blog posts by hand

| blogging, supernote

A- complains if I get screentime when she doesn't get screentime, so it's hard to find time to write on my laptop or on my phone. I've experimented with dictation before, since Google Recorder can make a half-decent transcript. I'm not used to talking things out, though. I keep correcting false starts, stutters, and mis-recognized words.

Fortunately, I can write on my A5X while waiting for A-. I get more space than I do when writing on my phone, so it's easier for me to think. I can export pages as PNGs, Dropbox, share each page, sync with with Google Photos, and then use Lens to copy the text. I can then paste it into Orgzly, which automatically syncs with Syncthing so that I can edit it on my laptop with Emacs. It needs a little cleanup (capitalization, stray punctuation, missed words, things in the wrong order), but editing it feels easier than dealing with the output of speech recognition, so it seems to be worth the extra time and effort. Besides, it feels less embarrassing to write at the sandbox than it is to talk to myself.

I can edit the text directly on my phone, but I still need my laptop to publish my blog because I haven't set up my static site generator on my server. Some day! In the meantime, this might be a good workflow for getting thoughts out there.

What if I want to refer to sketches while I write? Flipping between pages on the A5X can be challenging if they're not next to each other, but I can keep my current writing page next to my sketch. I could also view the sketch on my phone and balance it on the A5X, or use layers to keep a small version of the sketch as a handy reference. Lots of ideas to play around with…

Trying out the SuperNote A5X

| geek, drawing, supernote

W- was happy with his SuperNote A5X, so I ordered one for myself on July 18. The company was still doing pre-orders because of the lockdowns in China, but it shipped out on July 20 and arrived on July 25, which was pretty fast.

I noticed that the org-epub export makes verse blocks look double-spaced on the SuperNote, probably because <br> tags are getting extra spacing. I couldn't figure out how to fix it with CSS, so I've been hacking around it by exporting it as a different class without the <br> tags and just using { white-space: pre }. I also ended up redoing the templates I made in Inkscape, since the gray I used was too light to see on the SuperNote.

It was very tempting to dive into the rabbithole of interesting layouts on /r/supernote and various journaling resources, but I still don't have much time, so there's no point in getting all fancy about to-do lists or trackers at the moment. I wanted to focus on just a couple of things: untangling my thoughts and sketching. Sketchnoting books would be a nice bonus (and I actually managed to do one on paper during a recent playdate), but that can also wait until I have more focused time.

I've had the A5X for five days and I really like it. Writing with the Lamy pen feels like less work than writing with a pencil or regular pen. It's smooth but not rubbery. I've still been drawing in landscape form because that feels a little handier for reviewing on my tablet or writing about on my blog, but I should probably experiment with portrait form at some point.

So far, I've:

sketched out my thoughts
I used to use folded-over 8x14" to sketch out two thoughts, but scanning them was a bit of a pain. Sometimes I used the backs of our writing practice sheets in order to reduce paper waste, but then scanning wasn't always as clean. I really like using the SuperNote to sketch out thoughts like this one. It's neat, and I can get the note into my archive pretty easily.
sketched stuff from life
This is easier if I take a quick reference picture on my phone. I could probably even figure out some kind of workflow for making that available as a template for tracing.
received many kiddo drawings
A- loves being able to use the eraser and lasso to modify her drawings. Novelty's probably another key attraction, too. She's made quite a few drawings for me, even experimenting with drawing faces from the side like the way she's been seeing me practice doing.
received many kiddo requests
A- likes to ask me to draw things. She enjoys tracing over them in another layer. More drawing practice for both of us!
used it to help A- practise coding, etc.
A- wanted to do some coding puzzles with her favourite characters. I enjoyed being able to quickly sketch it up, drawing large versions and then scaling down as needed.
played a game of chess
I drew chess pieces just to see if I could, and we ended up using those to play chess. I should share these and maybe add other games as well.
referred to EPUBs and PDFs
I put our favourite songs and poems on it. I've also started using org-chef to keep a cookbook.
doodled sketch elements
boxes, borders, little icons, people… Probably should organize these and share them too.

I've figured out how to publish sketches by using my phone to rotate them and sync them with my online sketches. Now I'm playing around with my writing workflow to see if I can easily post them to my blog. At some point, I think I'll experiment with using my phone to record and automatically transcribe some commentary, which I can pull into the blog post via some other Emacs Lisp code I've written. Whee!