2
\$\begingroup\$

I just started messing around with Java's BufferedImage. I had an idea to try to encode a message into a picture by coloring each pixel a certain color that corresponds to a certain character.

Here's the above paragraph as a picture:

First paragraph as a picture.

It took about 18 minutes to generate. Note the black parts. Those are spaces.

And here's the Wikipedia article for Steganography:

Steganography article as a picture

I needed a way of enumerating colors to match colors to characters.

To do this, I decided to treat the RGB color as a 3-digit, base-255 number, then just increment each channel as though I was counting (increment the rightmost channel until it gets to 255, then make it 0 and increment the channel to the left). I used this for a previous project, and although it's slow, it was acceptable.

(defn inc-permutation
  "\"Counts\" arbitrary symbols.
  Example: (inc-permutation \\a \\c #(char (inc (int %))) [\\a \\b \\b])
  returns [\\a \\b \\c], then [\\a \\c \\a], [\\a \\c \\b], [\\a \\c \\c], [\\b \\a \\a], [\\b \\a \\lein pomb]...
  Quite slow."
  [first-symbol last-symbol inc-f current-permutation]
  (let [current-ones (last current-permutation)
        carry? (= current-ones last-symbol)
        overflown? (empty? current-permutation)
        last-i (dec (count current-permutation))]

    (cond
      carry? (conj
               (inc-permutation first-symbol last-symbol inc-f (subvec current-permutation 0 last-i))
               first-symbol)

      overflown? []

      :else (assoc current-permutation last-i (inc-f current-ones)))))

It works, but it seems like a hack more than anything.

I calculated that to cover the entire range of colors with a char, I would need to increment each color 174540 times * (- (int (character) 32). At 88ms per increment, it will take a little over 8 seconds to increment a \~ to [253 3 19], which for a message of any length is unacceptable.

Below is the full code. The last method, color-of-char, is where it's all tied together.

(ns bits.image.color-encode.conversion-helper)

(def min-char-code 32)
(def max-char-code "inclusive" 127)

(def total-colors (int (Math/pow 255 3)))
(def color-code-mult (int (inc (/ total-colors (- max-char-code min-char-code)))))

(def starting-color [0 0 0])

(defn inc-permutation ...)

(defn advance-color [color color-step]
  (let [f #(g/inc-permutation 0 255 inc %)]
    (if (= color-step 1)
      (f color)
      (reduce (fn [c _] ; Faster than `iterate`
                (f c))
              color
              (range color-step)))))

(defn color-of-char [chr]
  (let [code (- (int chr) min-char-code)]
    (advance-color [0 0 0]
                   (* code color-code-mult))))

(color-of-char \a) -> [173 29 13], but it takes an average of 5.879696 seconds to get there (according to Criterium).

  1. Is there anything I can do to speed up the current way? Any general comments would be welcome too.

  2. Is there a better way to "increment a RGB color"? Alternatively, is there a way to get some nth color, that isn't O(n)?

Edit:

I managed to get a mostly working constant time conversion algorithm written that relies on rem and division. It's O(n) with respect to the number of places in the color, which is much better. Here's this entire question as a picture:

enter image description here

It took about a quarter second to generate. The darker parts are low value characters like punctuation and numbers. They probably correspond to the code.

I'm still interested in any feedback though, since the new solution is still a work in progress.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

It is far faster to think of the RGB code as eight-bit fields of one whole number. A function to chop a number into them is

(defn number->rgb [n]
  (loop [acc [], n n, i 3]
    (if (zero? i)
      (vec (rseq acc))
      (recur (conj acc (mod n 256)) (quot n 256) (dec i)))))

It might be a little faster to use bit shifting and masking, but it hardly seems worth it.

We can then define our own

(defn my-color-of-char [chr]
  (let [code (- (int chr) min-char-code)]
    (number->rgb (* code color-code-mult))))

We can test this, for example, by

((juxt color-of-char my-color-of-char) \space)
;[[0 0 0] [0 0 0]]

and

((juxt color-of-char my-color-of-char) \Z)
;[[154 120 114] [154 120 114]]

As for speed, pre-computing the whole character map

(def char->RGB-map
  (->> (range 32 256)
       (map (juxt char (comp my-color-of-char)))
       (into {})))

... is instant at the REPL.

Let's check

(map char->RGB-map [\space \Z])
;([0 0 0] [154 120 114])

... as before.

\$\endgroup\$
1
  • \$\begingroup\$ Very neat. Never seen rseq before, so I'll need to look into that. Thanks. I just finished the decoding logic, so I'm going to get everything hooked up, test it, then try switching my functions with yours. \$\endgroup\$ Commented Jan 21, 2017 at 19:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.