How to Turn Picture Into Black and White Pixel Art

Converting an Epitome into ASCII Art Masterpiece

Published on 28 December 2017

While browsing Stack Overflow, I generally click on one or two links from the sidebar "Hot Network Questions". It brings me to several interesting topics, not necessarily related to development. And this time, I plant an interesting mail service: how do ASCII art epitome conversion algorithms work?

ASCII art image conversion basically consists in two steps: converting our pic into grey colors, and map each pixel to a given character depending of the grayscale value. For example, @ is darker than +, which is also darker than .. So, let's try to implement such an algorithm in pure JavaScript.

For those in a hurry, y'all tin can exam the converter direct in concluding demo, or read its source code directly on its GitHub repository.

Uploading an Image into a Sail

Homer Simpson

Starting time pace is to allow our user to upload a motion picture. Hence, nosotros demand a file input. Moreover, every bit we are going to manipulate prototype pixels, nosotros also need a sail.

                                  <!DOCTYPE html>                  <html                  lang=                  "en"                  >                  <head>                  <meta                  charset=                  "UTF-viii"                  >                  <meta                  name=                  "viewport"                  content=                  "width=device-width, initial-scale=1.0"                  >                  <meta                  http-equiv=                  "10-UA-Compatible"                  content=                  "ie=border"                  >                  <championship>Ascii Art Converter</championship>                  </head>                  <body>                  <h1>Ascii Art Converter</h1>                  <p>                  <input                  blazon=                  "file"                  name=                  "picture"                  />                  </p>                  <canvas                  id=                  "preview"                  ></sheet>                  </body>                  </html>                              

At this step, we can send a film to our input, withal null would happen. Indeed, we need to plug the file input to our canvas element. It is done using the FileReader API:

                                  const                  canvas                  =                  document                  .                  getElementById                  (                  '                  preview                  '                  );                  const                  fileInput                  =                  document                  .                  querySelector                  (                  '                  input[type="file"                  '                  );                  const                  context                  =                  canvas                  .                  getContext                  (                  '                  2nd                  '                  );                  fileInput                  .                  onchange                  =                  (                  e                  )                  =>                  {                  // just handling single file upload                  const                  file                  =                  east                  .                  target                  .                  files                  [                  0                  ];                  const                  reader                  =                  new                  FileReader                  ();                  reader                  .                  onload                  =                  (                  upshot                  )                  =>                  {                  const                  image                  =                  new                  Image                  ();                  image                  .                  onload                  =                  ()                  =>                  {                  sheet                  .                  width                  =                  image                  .                  width                  ;                  canvas                  .                  summit                  =                  image                  .                  acme                  ;                  context                  .                  drawImage                  (                  paradigm                  ,                  0                  ,                  0                  );                  }                  prototype                  .                  src                  =                  event                  .                  target                  .                  result                  ;                  };                  reader                  .                  readAsDataURL                  (                  file                  );                  };                              

On input change, we instantiate a new FileReader which would read the file, and in one case done, would load it into our sheet. Note we adapt the canvass size to the image one to not truncate it. The last 2 arguments of drawImage is the top left image margin: we want to start drawing our prototype from the top left corner (coordinates [0, 0]).

If we embed previous script on our HTML folio and upload Homer, we can brandish it into our canvas element:

Upload Preview in Canvas

Note: if you want to snap a picture from your webcam, delight refer to Taking Motion picture From Webcam Using Sheet post.

Turning an Prototype into Grayness Colors

Now our image has been uploaded, we demand to convert it into grayness colors. Each pixel color can be broken into three singled-out components: red, green, and blueish values, equally in hexadecimal (#RRGGBB) colors in CSS. Computing grey scale of a pixel is simply averaging these iii values together.

However, human heart is not equally sensitive to these 3 colors. For instance, our optics are very sensitive to green color, while blue is only slightly perceived. Hence, we need to ponderate each colors using dissimilar weights. After taking a wait on the (very) detailed Grayscale Wikipedia Folio, nosotros tin can compute the grayscale value using the following formula:

                GrayScale = 0.21 R + 0.72 G + 0.07 B                              

So, we need to iterate on each of our pic pixel, extract its RGB components, and supervene upon each component by its related grayscale value. Fortunately, working on a sail allows u.s.a. to dispense each pixel using getImageData function.

                                  const                  toGrayScale                  =                  (                  r                  ,                  m                  ,                  b                  )                  =>                  0.21                  *                  r                  +                  0.72                  *                  g                  +                  0.07                  *                  b                  ;                  const                  convertToGrayScales                  =                  (                  context                  ,                  width                  ,                  top                  )                  =>                  {                  const                  imageData                  =                  context                  .                  getImageData                  (                  0                  ,                  0                  ,                  width                  ,                  superlative                  );                  const                  grayScales                  =                  [];                  for                  (                  permit                  i                  =                  0                  ;                  i                  <                  imageData                  .                  data                  .                  length                  ;                  i                  +=                  4                  )                  {                  const                  r                  =                  imageData                  .                  data                  [                  i                  ];                  const                  g                  =                  imageData                  .                  information                  [                  i                  +                  1                  ];                  const                  b                  =                  imageData                  .                  data                  [                  i                  +                  ii                  ];                  const                  grayScale                  =                  toGrayScale                  (                  r                  ,                  g                  ,                  b                  );                  imageData                  .                  data                  [                  i                  ]                  =                  imageData                  .                  information                  [                  i                  +                  i                  ]                  =                  imageData                  .                  data                  [                  i                  +                  2                  ]                  =                  grayScale                  ;                  grayScales                  .                  push                  (                  grayScale                  );                  }                  context                  .                  putImageData                  (                  imageData                  ,                  0                  ,                  0                  );                  render                  grayScales                  ;                  };                              

The for loop requires some explanations. We retrieve each pixels in the imageData.information object. However, it is a uni-dimensional array, each pixel beingness splitted into its four components: red, green, blue, and alpha (for transparency). We retrieve the RGB value from the three kickoff data cells, compute our grayscale, and then, movement on of 4 indexes to go at the start of the side by side pixel components.

In this snippet, we modified the original image data, causing our part to be impure. Indeed, I wasn't able to find a fashion to update image information using a copy of our imageData variable.

Adding a call to convertToGrayScales office at the stop of our image.onload listener, nosotros tin can upload a picture in grayness colors:

Grayscale Homer Preview

Mapping the Pixels to Gray Scale Values

Now that nosotros take a list of grayscale values for every pixel, nosotros can map each of these value to a unlike character. Reason behind this mapping is uncomplicated: some characters are darker than others. For instance, @ is darker than ., which occupies less space on screen.

The post-obit graphic symbol ramp is more often than not used for this conversion:

Hence, mapping a gray scale value to its equivalent character can be done via:

                                  const                  grayRamp                  =                  '                  [email protected]%viii&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()i{}[]?-_+~<>i!lI;:,"^`                  \'                  .                                    '                  ;                  const                  rampLength                  =                  grayRamp                  .                  length                  ;                  const                  getCharacterForGrayScale                  =                  grayScale                  =>                  grayRamp                  [                  Math                  .                  ceil                  ((                  rampLength                  -                  one                  )                  *                  grayScale                  /                  255                  )];                              

We retrieve the corresponding character using a cross-shaped product: grey scale of 0 (black) should be $, and a white pixel (gray scale of 255) should be a space. We substract 1 to rampLength every bit arrays start at 0 alphabetize.

Let's interpret our input image into pure characters:

                                  const                  asciiImage                  =                  document                  .                  querySelector                  (                  '                  pre#ascii                  '                  );                  const                  drawAscii                  =                  (                  grayScales                  )                  =>                  {                  const                  ascii                  =                  grayScales                  .                  reduce                  ((                  asciiImage                  ,                  grayScale                  )                  =>                  {                  render                  asciiImage                  +                  getCharacterForGrayScale                  (                  grayScale                  );                  },                  ''                  );                  asciiImage                  .                  textContent                  =                  ascii                  ;                  };                              

Nosotros utilize a pre tag in social club to keep aspect ratio of our picture, equally information technology uses a monospaced font.

Calling the drawAscii method at the finish of our paradigm.onload callback, we go the following result:

Drawing ASCII image without break lines

At first glance, it seems information technology doesn't work. Yet, if we curl horizontally, we detect some strings wandering through the screen. Our picture seems to be on a single line. And indeed: all our values are on a single dimensional array. Hence, nosotros need to add a break line every width value:

                                  const                  drawAscii                  =                  (                  grayScales                  ,                  width                  )                  =>                  {                  const                  ascii                  =                  grayScales                  .                  reduce                  ((                  asciiImage                  ,                  grayScale                  ,                  index                  )                  =>                  {                  let                  nextChars                  =                  getCharacterForGrayScale                  (                  grayScale                  );                  if                  ((                  alphabetize                  +                  i                  )                  %                  width                  ===                  0                  )                  {                  nextChars                  +=                  '                  \n                  '                  ;                  }                  return                  asciiImage                  +                  nextChars                  ;                  },                  ''                  );                  asciiImage                  .                  textContent                  =                  ascii                  ;                  };                              

Result is now far ameliorate, except for a detail…

Drawing far too big ASCII image

Our epitome ASCII representation is huge. Indeed, nosotros mapped any single pixel to a character, spread on a lot of pixels. Cartoon a 10x10 small moving picture would then accept 10 lines of 10 characters. Also large. We tin can of form keep this huge text moving-picture show and reduce font-size as shown in previous picture. Even so, that's not optimal, especially if yous want to share it past email.

Lowering ASCII Image Definition

When browsing the Web to bank check how other reach such a resolution downgrade, we oft observe the boilerplate method:

Computing Average Pixels Value on Image

This technique consists in taking sub-arrays of pixels and to compute their average grayscale. And so, instead of drawing nine white pixels for the red department above, nosotros would draw a unmarried one, still completly white.

I first dove into the code, trying to compute this average on the unidimensional array. Notwithstanding, afterward an hour of tying myself in knots, I remembered the adjacent two arguments of drawImage canvas method: the output width and height. Their main goal is to resize moving picture before drawing it. Exactly what nosotros have to do! I wasn't able to detect how this is done under the hood, merely I guess this is using the same boilerplate process.

Let's clamp our epitome dimension:

                                  const                  MAXIMUM_WIDTH                  =                  80                  ;                  const                  MAXIMUM_HEIGHT                  =                  l                  ;                  const                  clampDimensions                  =                  (                  width                  ,                  height                  )                  =>                  {                  if                  (                  height                  >                  MAXIMUM_HEIGHT                  )                  {                  const                  reducedWidth                  =                  Math                  .                  floor                  (                  width                  *                  MAXIMUM_HEIGHT                  /                  tiptop                  );                  render                  [                  reducedWidth                  ,                  MAXIMUM_HEIGHT                  ];                  }                  if                  (                  width                  >                  MAXIMUM_WIDTH                  )                  {                  const                  reducedHeight                  =                  Math                  .                  floor                  (                  acme                  *                  MAXIMUM_WIDTH                  /                  width                  );                  return                  [                  MAXIMUM_WIDTH                  ,                  reducedHeight                  ];                  }                  return                  [                  width                  ,                  height                  ];                  };                              

We focus on summit first. Indeed, to meliorate appreciate the creative person behind their work, we need to contemplate their art without scrolling. Also annotation that we go on image aspect ratio to prevent some weird distortions. We at present need to update our image.onload handler to use the clamped values:

                                  image                  .                  onload                  =                  ()                  =>                  {                  const                  [                  width                  ,                  summit                  ]                  =                  clampDimensions                  (                  paradigm                  .                  width                  ,                  image                  .                  height                  );                  canvas                  .                  width                  =                  width                  ;                  canvas                  .                  elevation                  =                  height                  ;                  context                  .                  drawImage                  (                  image                  ,                  0                  ,                  0                  ,                  width                  ,                  height                  );                  const                  grayScales                  =                  convertToGrayScales                  (                  context                  ,                  width                  ,                  height                  );                  drawAscii                  (                  grayScales                  ,                  width                  );                  };                              

If we upload our favorite Simpson character, here is the result:

              U88f                                              mr kzB   C'                                             8  f   @   t                                             ^  [email protected]!l!{o%                                            westward c#1)i!!!!!!!!B                                            [email protected])[!!!!!!!!!IW                                            @)1Y)!!!!!!!!!!!,B                                           @o)))[!!!!!!!!!!!!"J                                          "1))))!!!!!!!!!!!!!l|                                          @)))))!!!!!!!!!!!!!!"|                                          u)))))!!!!!!!!!!!!!50,@                                         <1)))))!!!!!!!!!!!!!!lf                                         Y1)))))!!!!!!!!!!!!!!!I                                         C))))))!!!!!!!!!!!!!!!"X                                         ())))))i!!!!l!!!!!!l!ll"10                                         `1)))))?!&]  }&!!)q   p]?                                          t)))))1|      pU       j                                          a)))))0        @       f                                          #))))q         '        ^                                          i))))@  a8      !    <@ l                                           t)ane)W  li      !      .                           :                                           8)d1W         "`[email protected] %                         %11x]                                         ~*@one)@)         @^;ll,|j                         %))[!Thousand)LI             '&zo!                       ^:fx)X)*       O!!!!!l~^"                       cc/!!J)]~x             j)!llO                        B*))f)Q{   'B!!!!!!]@;10                      B{{i*W1]!!!q          "MUB1}!!l{                      ' Z))))<!>(?!!!))){0*<@northward                     b1{!!!<[e-mail protected]@          j!!!Z1*d))@                       q))))-!!>#WwLCm0ft??]!t*[email protected]               U)[email protected]))*         %[electronic mail protected]+!!!iB                     8)%)))[email protected]/t/}}11]???????]W-?f              :1}Cl!!l,B)1!!!X         p))!!f{!!!!!+                    Westward!i!))){&f]??????????????????Y              Q)>!one!!!:1}!!!l8        @~jB)<*!!!,f!;k                   xvoh)))@t?????????????????]B?B              %)!lZ","%)[electronic mail protected]!W       L!!!!<Q|!!!ll!!q                   thousand)L))))t)[email protected])*             Y))!!!kBaM~!xCxIx       B!!!!!>c!!,viii!!!"B                  IX11Y)#t??????????????????]f]8            81))!!xl!!!MI_#!u       B)!!!!!%[electronic mail protected]!!!!!">b                  ?#))%t????????????????????-0          ~h)))_!!h!!!!!i!i^Y       West)@|[email protected]!!lx!!!!!!"Y(                 eight))af???]????????????|B{{@          G)))){!!!!!!!!!!i"@       'ff/|)xt1!!O!!!!!!!!"w!               @))Wf?????????????????%           -*))))?!!!!!!!!!!,"eight        m11kb1))!!!!!!!!!!!!!"*;             @))8t????????????????%          ;@xi)))!!!!!!!!!!!"xf         o1))))))!!!!!!!!!!!!!I"@            @)))[email protected]         [electronic mail protected])))){!!!!!!!!!!!"@          /yard)))))]!!!!!!!!!!!!!!I,@          B)))&/???????????]]q        JM1)))))>!!!!!!!!!!!"%            bq))))1!!!!!!!!!!!!!!!I,&        W))))West)??????????West:    ` IBY))))))-!!!!!!!!!!!!,B              @1))))!!!!!!!!!!!!!!!!I;& d.   Z)))))[email protected]}?????}@-<   nJuB1)))))){!!!!!!!!!!!!!"eight               ninety)))){!!!!!!!!!!!!!!!!l)h]@  ())11)>ilrh&m/l!^"  a!lll81)))}!!!!!!!!!!!!!!"&                 B)))))-!!!!!!!!!!!!!!!!!(fifty*#@#X+l     X<!!!!,qQmqlllllC1[!!!!!!!!!!!!!!!"@                  h()))))!!!!!!!!!!!!!!!]  lLilll'    ..}i!!!:Il [lllll:50!!!!!!!!!!!!!!!"@                   ,*)))))}!!!!!!!!!!!!50%   lklll     West  q!!!!I?  ~ll"   8!!!!!!!!!!!!!"8                     &))))))!!!!!!!!!!!!Westward    l$fifty,     p   $!!lq   J^      b!!!!!!!!!!:"k                      >d)))))[!!!!!!!!!+      I}      '    [email protected];   o       :l!!!!!!!!"+]                        @one)))))!!!!!!!!B      `o     |     ]U  B  K        B!!!!!!I"o.                         Uc)))))<!!!!!i        I|JbooB         ^. o        .>!!!!",a                          .B)))))}!!!!a         B               @'          @!!:"One thousand`                            bf)))))!!O.         t               1           >I"1Y                             ^&)))))i'          .                _           _8                               @)))1Z                            B           0                                1Z)@l                            C          `;                                .;$lll`                          .         Q>                                 @llllll                          ?      {a                                  zrlllll^                        *   +%x                                    Zh;llll.                     [electronic mail protected]                                      ./MBW8z                      %            

Resolution has been decreased and we can't see every bit many details as before, but that's a mandatory drawback to go shareable ASCII art.

As usual, hither are the related links:

  • Last ASCII Fine art Converter Demo,
  • GitHub Repository

Notation that we only handle static prototype in this example, only some people too handle live video stream, such as the ASCII camera. Useless, therefore indispensable!

willardhispout87.blogspot.com

Source: https://www.jonathan-petitcolas.com/2017/12/28/converting-image-to-ascii-art.html

0 Response to "How to Turn Picture Into Black and White Pixel Art"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel