Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw
 
PerlMonks  

Re^2: Imagecat - show color images in a terminal

by Anonymous Monk
on Jul 05, 2023 at 11:55 UTC ( [id://11153283]=note: print w/replies, xml ) Need Help??


in reply to Re: Imagecat - show color images in a terminal
in thread Imagecat - show color images in a terminal

I think I exhausted this exercise as distraction source, better call it a day and publish. Know more now that I don't know than I knew that I didn't knew, as usual :). Plus a few factoids about weirdness/bugs in Imager/PDL/FreeType.

Console in screenshots was configured to use fonts as seen in commands before they were run. "Wrong setup" is console configured to use DejaVuSansMono and command to calculate for Consolas. Which was exactly the point -- to demo (for self) that color management does work even with very limited resources = size and composition of palette to pick best match. Google image search fails anyway to find the original, which is Wiki title pic of Kelly, of course (for ye movie fans out here :)

I don't know if yellow shift and more posterization for DejaVuSansMono is either because FreeType renders glyphs differently from Windows, or, perhaps, 50% fill for medium shade (as opposed to other fonts) means scarce repertoire of tints closer to quarter-tones in palette, for skin.

use strict; use warnings; use Term::ANSIColor 'colored'; use Term::ReadKey 'GetTerminalSize'; use Imager ':handy'; use PDL; use PDL::IO::Image; use PDL::Transform::Color; use PDL::Graphics::ColorDistance 'delta_e_2000'; ################################## ########### Deal with usage ################################## my ( $image_file, $font_file ) = @ARGV; die "Usage: $0 \"image file\" \"optional font file\"\n" unless $image_file; ################################## ########### Deal with terminal ################################## use if $^O eq 'MSWin32', 'Win32::Console'; my $out; if ( $^O eq 'MSWin32' ) { $out = Win32::Console-> new( STD_OUTPUT_HANDLE()); $out-> OutputCP( 65001 ); $out-> Mode( $out-> Mode | 4 ); # Win10 } binmode STDOUT, ':utf8'; $_ -- for my ( $cols, $rows ) = GetTerminalSize; $rows --; $rows --; # fit command in screenshot ################################## ########### Configurable stuff ################################## my $colors = << 'END_OF_COLORS'; 0 0 0 black 1 1 1 bright_white 1 0 0 bright_red 1 1 0 bright_yellow 0 1 0 bright_green 0 1 1 bright_cyan 0 0 1 bright_blue 1 0 1 bright_magenta 0.5 0.5 0.5 bright_black 0.75 0.75 0.75 white 0.5 0 0 red 0.5 0.5 0 yellow 0 0.5 0 green 0 0.5 0.5 cyan 0 0 0.5 blue 0.5 0 0.5 magenta END_OF_COLORS # ^ Presumably, (1/) for 8 color terminal, # use only 8 rows above (and delete 'bright_'); # (2/) for 16 color terminal configured with a palette, # edit values to match palette my $distortion = 2.0; # Not adjusted below (assume constant) my $font_size = 100; my $dont_mix = 0; # "1" is don't mix RGB, use only # colored chars on grey, or grey on color my @chars = ( "\N{SPACE}", # the defaults, "\N{LIGHT SHADE}", "\N{MEDIUM SHADE}", # unless font file "\N{DARK SHADE}", "\N{MIDDLE DOT}" ); # is given # my @weights = ( 0, 0.18, 0.49, 0.81, 0.03 ); # (DejaVuSansMono values) my @chars_to_try = ( @chars, "\N{PROPORTION}", "\N{DOTTED CIRCLE}", "\N{BULLET}", "\N{BULLSEYE}", "\N{CIRCLED DOT OPERATOR}" ); ################################## ########### Deal with font ################################## if ( $font_file ) { my $font = NF( file => $font_file, size => $font_size, aa => 1, color => NC( grey => 255 ), ); my $bbox = $font-> bounding_box( string => 'a' ); # any my ( $w, $h, $baseline ) = map $bbox-> $_, qw( advance_width font_height global_ascent ); my $cell = Imager-> new( xsize => $w, ysize => $h, channels => 1 ) +; @chars = @weights = (); for my $char ( @chars_to_try ) { next unless "\1" eq $font-> has_chars( string => $char ); $cell-> box( filled => 1, color => NC( grey => 0 )); $cell-> string( x => 0, y => $baseline, font => $font, string => $char ); my ( $fill ) = $cell -> scale( xpixels => 1, ypixels => 1, type => 'nonprop', qtype => 'mixing' ) -> getsamples( y => 0 ); push @chars, $char; push @weights, $fill / 255 } } ################################## ########### Deal with image ################################## my $image; { my $pimage = PDL::IO::Image-> new_from_file( $image_file ) -> rescale_pct( 100, 100 / $distortion ); my $w_r = $cols / $pimage-> get_width; my $h_r = $rows / $pimage-> get_height; my $ratio = $w_r < $h_r ? $w_r : $h_r; $pimage-> rescale_pct( 100 * $ratio ) if $ratio < 1; $image = $pimage-> pixels_to_pdl-> mv( -1, 0 ); } ################################## ########### Deal with palette ################################## my ( @composition, @color_names ); for ( split "\n", $colors ) { next unless /(?=\pL)/; push @composition, $`; push @color_names, $'; } my $compo = 255 * pdl join ';', @composition; my @pixels; my $palette_str = ''; my %seen; my $n = @color_names; for my $w_i ( 0 .. $#weights ) { my $fg = $compo * $weights[ $w_i ]; my $bg = $compo * ( 1 - $weights[ $w_i ]); my $table = $bg-> dummy( 1, $n ) + $fg-> dummy( 1, $n )-> xchg( 1, 2 ); my $triplets = byte $table-> clump( 1, 2 ); my $triplets_str = ${ $triplets-> get_dataref }; for my $bg_i ( 0 .. $#color_names ) { for my $fg_i ( 0 .. $#color_names ) { my $rgb = substr $triplets_str, 0, 3, ''; next if $seen{ $rgb } ++; next if $dont_mix and $color_names[ $fg_i ] !~ /black|white/ and $color_names[ $bg_i ] !~ /black|white/; $palette_str .= $rgb; push @pixels, colored( $chars[ $w_i ], "$color_names[ $fg_i ] on_$color_names[ $bg_i ]" ) } } } my $plt_size = scalar @pixels; my $palette = zeroes byte, 3, $plt_size; ${ $palette-> get_dataref } = $palette_str; $palette-> upd_data; ################################## ########### Ready to go ################################## $image = t_lab-> apply(( !t_srgb )-> apply( $image )); $palette = t_lab-> apply(( !t_srgb )-> apply( $palette )); my $distance = delta_e_2000( $palette, $image-> dummy( 1, $plt_size )) +; print @pixels[ @$_ ], "\n" for @{ $distance-> minimum_ind-> unpdl }; __END__

Replies are listed 'Best First'.
Re^3: Imagecat - show color images in a terminal
by Tux (Canon) on Jul 06, 2023 at 07:41 UTC

    Nice, but trying to play with this I found that it requires PDL::IO::Image which depends on Alien::FreeImage which won't build on modern g++ (it requires -std=c++14 and redefines powf64) and it has security issues (see issues).

    Other than that, thanks! Some things to look back at when I encounter similar tasks.


    Enjoy, Have FUN! H.Merijn

      I suspected result on Linux be wilder than "wrong_setup" because terminals are almost certain to use palette, not pure RGB. "Security"? Life corrects as always.

      ################################## ########### Deal with image ################################## use PDL::IO::FlexRaw; my $image; { my $im = Imager-> new( file => $image_file ) -> to_rgb8 -> convert( preset => 'noalpha' ) -> scale( xscalefactor => 1, yscalefactor => 1 / $distortion ); my $w_r = $cols / $im-> getwidth; my $h_r = $rows / $im-> getheight; my $ratio = $w_r < $h_r ? $w_r : $h_r; $im = $im-> scale( scalefactor => $ratio ) if $ratio < 1; $im-> write( data => \my $buf, type => 'raw' ); my $dims = [ 3, $im-> getwidth, $im-> getheight ]; open my $fh, '<:raw', \$buf; $image = readflex( $fh, [{ Type => 'byte', Dims => $dims }]) }

      TIMTOWTDI. One reason to use PDL::IO::Image was to play with its rescale filters (which amounted to nothing) to average glyph bitmap to single pixel. In the end it's arithmetic mean with Imager::scale(qtype => 'mixing',... and itself not necessary neither, could be unpack '%32C*', ...

      (Another reason for PDL::IO::Image was to use the best what happened to image IO and basic manipulation, for PDL if not Perl. Very sad if it's abandoned. PDL::IO::GD and PDL::IO::Pic don't compare)

        Computations above take noticeable time, as opposed to simple algorithm in OP. If final section is formatted as:

        ################################## ########### Ready to go ################################## use Time::HiRes 'time'; my $t = time; $image = t_lab-> apply(( !t_srgb )-> apply( $image )); $palette = t_lab-> apply(( !t_srgb )-> apply( $palette )); my $indexed = delta_e_2000( $palette, $image-> dummy( 1, $plt_size ) )-> minimum_ind; print time - $t, "\n"; print @pixels[ @$_ ], "\n" for @{ $indexed-> unpdl };

        then time is 4.56991696357727 for, ultimately, a single Perl statement; using e.g. DejaVuSansMono and console/font size approximately as used to produce picture above. Not cheap. For 10 brushes (i.e. glyphs from this font) and all combinations of FG/BG colors (16*16), the palette size upper limit is 2560. It was reduced to 'only' 1854, excluding trivial duplicates. Distorted image size is 142 by 89.

        For each of its 12638 pixels, calculate 1854 distances with

        https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
        and pick the best palette index, i.e. ~23e6 times. Maybe we are lucky we have PDL, and this horrible formula was already implemented in C.

        Unfortunately, it's immediately obvious it'll take same long time if image is a flat single color rectangle (or simple logo, etc.), while it must be 12638 times faster!

        For our test image, distorted and scaled to 142x89, only half (6265 out of 12638) RGB triplets are unique (Imager::getcolorcount). It follows, run time should be twice as low.

        I didn't find anything w.r.t. 'memoization' and PDL, maybe not tried hard enough? Duplication in arrays and expensive computations are inevitable. It only requires to reduce fat data to unique slim set, keeping track how to fatten result to original shape. Not 'memoization' per definition, but in spirit.

        ################################## ########### Ready to go ################################## use Time::HiRes 'time'; my $t = time; my ( undef, $w, $h ) = $image-> dims; my $fat_rgb = $image-> clump( 1, 2 ); my $perm_2_sort = $fat_rgb-> qsortveci; my $sorted = $fat_rgb-> dice_axis( 1, $perm_2_sort ); my $enumerated = $sorted-> enumvecg; my $selector = $enumerated-> uniqind; my $slim_rgb = $sorted-> dice_axis( 1, $selector ); $slim_rgb = t_lab-> apply(( !t_srgb )-> apply( $slim_rgb )); $palette = t_lab-> apply(( !t_srgb )-> apply( $palette )); my $slim_indexed = delta_e_2000( $palette, $slim_rgb-> dummy( 1, $plt_size ) )-> minimum_ind; my $indexed = $slim_indexed -> index( $enumerated ) # fatten -> index( $perm_2_sort-> qsorti ) # permute to original -> reshape( $w, $h ); # reshape :) print time - $t, "\n"; print @pixels[ @$_ ], "\n" for @{ $indexed-> unpdl };

        And time now is exactly as predicted: 2.25233697891235. (PDL has uniq and uniqind pair. But only uniqvec i.e. no uniqvecind. Hence workaround with vector enumeration)

        ###############

        In fact, RGB (or Lab) points in palette are spaced so very sparsely. If further speed is required, some precision can be sacrificed, and even slimmer dataset fed to expensive formula. With

        $fat_rgb-> sever-> inplace-> and2( 0b1111_1110 );

        i.e. RGB in image reduced to 7 bits (if GD always does it (to alpha), why can't we?), time becomes 1.3685450553894. With 2 bits zeroed, it's 0.765566825866699; and result is practically the same if observer was not warned. With reduction to 5 bits (again 2x faster) the picture is beginning to feel slightly uncomfortable compared to 'lossless' result.

        ###############

        W.r.t. need to sever above, I'm confused if clump creates dataflow. But what follows looks like plain bugs:

        pdl> $x = zeroes 3,2,2; $y = $x-> clump( 1,2 ) pdl> $x .= 3 pdl> p $x, $y, $y-> uniqvec [ [ [3 3 3] [3 3 3] ] [ [3 3 3] [3 3 3] ] ] [ [3 3 3] [3 3 3] [3 3 3] [3 3 3] ] [ [3 3 3] ] pdl> $x = zeroes 3,2,2; $y = $x-> clump( 1,2 ) pdl> $y .= 3 pdl> p $x, $y, $y-> uniqvec [ [ [3 3 3] [3 3 3] ] [ [3 3 3] [3 3 3] ] ] [ [3 3 3] [3 3 3] [3 3 3] [3 3 3] ] [ [0 0 0] ]

        So far, only uniqvec looks weird. But:

        pdl> $x = zeroes 3,2,2; $y = $x-> clump( 1,2 ) pdl> p $y [ [0 0 0] [0 0 0] [0 0 0] [0 0 0] ] pdl> $x .= 3 pdl> p $x, $y, $y-> uniqvec [ [ [3 3 3] [3 3 3] ] [ [3 3 3] [3 3 3] ] ] [ [0 0 0] [0 0 0] [0 0 0] [0 0 0] ] [ [0 0 0] ]

        What?! But I only printed the $y, compared to 1st demo.

        Using FlexRaw to read the PV of a scalar as an ndarray is very clever, nice work! However, a more idiomatic way might be to replace the last 4 lines with (untested):
        $image = zeroes byte, $im->getwidth, $im->getheight; $im->write( data => $image->get_dataref, type => 'raw' ); $image->upd_data;
        Another way to read images into PDL with lots of capability is PDL::OpenCV::Imgcodecs.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://11153283]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (4)
As of 2025-06-20 14:02 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?
    erzuuliAnonymous Monks are no longer allowed to use Super Search, due to an excessive use of this resource by robots.