Detect if video is black and white in bash - bash

I have a folder with hundreds of films, and I'd like to separate the color ones from black and white. Is there a bash command to do this for general video files?
I already extract a frame:
ffmpeg -ss 00:15:00 -i vid.mp4 -t 1 -r 1/1 image.bmp
How can I check if the image has a color component?

I never found out why video processing questions are answered on SO but as they typically are not closed, i'll do my best... As this is a developer board, i cannot recommend any ready commandline tool to use for your bash command, nor do i know any. Also i cannot give a bash only solution because i do not know how to process binary data in bash.
To get out if an image is grey or not, you'll need to check each pixel for it's color and "guess" if it is kind of grey. As others say in the comments, you will need to analyze multiple pictures of each video to get a more accurete result. For this you could possibly use the scene change detection filter of ffmpeg but thats another topic.
I'd start by resizing the image to save processing power, e.g. to 4x4 pixels. Also make sure you guarantee the colorspace or better pix_format is known so you know what a pixel looks like.
Using this ffmpeg line, you would extract one frame in 4x4 pixels to raw RGB24:
ffmpeg -i D:\smpte.mxf -pix_fmt rgb24 -t 1 -r 1/1 -vf scale=4:4 -f rawvideo d:\out_color.raw
The resulting file contains exactly 48 bytes, 16 pixels each 3 bytes, representing R,G,B color. To check if all pixels are gray, you need to compare the difference between R G and B. Typically R G and B have the same value when they are gray, but in reality you will need to allow some more fuzzy matching, e.g. if all values are the same +-10.
Some example perl code:
use strict;
use warnings;
my $fuzz = 10;
my $inputfile ="d:\\out_grey.raw";
die "input file is not an RGB24 raw picture." if ( (-s $inputfile) %3 != 0);
open (my $fh,$inputfile);
binmode $fh;
my $colordetected = 0;
for (my $i=0;$i< -s $inputfile;$i+=3){
my ($R,$G,$B);
read ($fh,$R,1);
$R = ord($R);
read ($fh,$B,1);
$B = ord($B);
read ($fh,$G,1);
$G = ord($G);
if ( $R >= $B-$fuzz and $R <= $B+$fuzz and $B >= $G-$fuzz and $B <= $G+$fuzz ) {
#this pixel seems gray
}else{
$colordetected ++,
}
}
if ($colordetected != 0){
print "There seem to be colors in this image"
}

Related

gnuplot: how to plot one 2D array element per pixel with no margins

I am trying to use gnuplot 5.0 to plot a 2D array of data with no margins or borders or axes... just a 2D image (.png or .jpg) representing some data. I would like to have each array element to correspond to exactly one pixel in the image with no scaling / interpolation etc and no extra white pixels at the edges.
So far, when I try to set the margins to 0 and even using the pixels flag, I am still left with a row of white pixels on the right and top borders of the image.
How can I get just an image file with pixel-by-pixel representation of a data array and nothing extra?
gnuplot script:
#!/usr/bin/gnuplot --persist
set terminal png size 400, 200
set size ratio -1
set lmargin at screen 0
set rmargin at screen 1
set tmargin at screen 0
set bmargin at screen 1
unset colorbox
unset tics
unset xtics
unset ytics
unset border
unset key
set output "pic.png"
plot "T.dat" binary array=400x200 format="%f" with image pixels notitle
Example data from Fortran 90:
program main
implicit none
integer, parameter :: nx = 400
integer, parameter :: ny = 200
real, dimension (:,:), allocatable :: T
allocate (T(nx,ny))
T(:,:)=0.500
T(2,2)=5.
T(nx-1,ny-1)=5.
T(2,ny-1)=5.
T(nx-1,2)=5.
open(3, file="T.dat", access="stream")
write(3) T(:,:)
close(3)
end program main
Some gnuplot terminals implement "with image" by creating a separate png file containing the image and then linking to it inside the resulting plot. Using that separate png image file directly will avoid any issues of page layout, margins, etc. Here I use the canvas terminal. The plot itself is thrown away; all we keep is the png file created with the desired content.
gnuplot> set term canvas name 'myplot'
Terminal type is now 'canvas'
Options are ' rounded size 600,400 enhanced fsize 10 lw 1 fontscale 1 standalone'
gnuplot> set output '/dev/null'
gnuplot> plot "T.dat" binary array=400x200 format="%f" with image
linking image 1 to external file myplot_image_01.png
gnuplot> quit
$identify myplot_image_01.png
myplot_image_01.png PNG 400x200 400x200+0+0 8-bit sRGB 348B 0.000u 0:00.000
Don't use gnuplot.
Instead, write a script that reads your data and converts it into one of the Portable Anymap formats. Here's an example in Python:
#!/usr/bin/env python3
import math
import struct
width = 400
height = 200
levels = 255
raw_datum_fmt = '=d' # native, binary double-precision float
raw_datum_size = struct.calcsize(raw_datum_fmt)
with open('T.dat', 'rb') as f:
print("P2")
print("{} {}".format(width, height))
print("{}".format(levels))
raw_data = f.read(width * height * raw_datum_size)
for y in range(height):
for x in range(width):
raw_datum, = struct.unpack_from(raw_datum_fmt, raw_data, (y * width + x) * raw_datum_size)
datum = math.floor(raw_datum * levels) # assume a number in the range [0, 1]
print("{:>3} ".format(datum), end='')
print()
If you can modify the program which generates the data file, you can even skip the above step and instead generate the data directly in a PNM format.
Either way, you can then use ImageMagick to convert the image to a format of your choice:
./convert.py | convert - pic.png
This should be an easy task, however, apparently it's not.
The following might be a (cumbersome) solution because all other attempts failed. My suspicion is that some graphics library has an issue which you probably cannot solve as a gnuplot user.
You mentioned that ASCII matrix data is also ok. The "trick" here is to plot data with lines where the data is "interrupted" by empty lines, basically drawing single points. Check this in case you need to get your datafile 1:1 into a datablock.
However, if it is not already strange enough, it seems to work for png and gif terminal but not for pngcairo or wxt.
I guess the workaround is probably slow and inefficient but at least it creates the desired output. I'm not sure if there is a limit on size. Tested with 100x100 pixels with Win7, gnuplot 5.2.6. Comments and improvements are welcome.
Code:
### pixel image from matrix data without strange white border
reset session
SizeX = 100
SizeY = 100
set terminal png size SizeX,SizeY
set output "tbPixelImage.png"
# generate some random matrix data
set print $Data2
do for [y=1:SizeY] {
Line = ''
do for [x=1:SizeX] {
Line = Line.sprintf(" %9d",int(rand(0)*0x01000000)) # random color
}
print Line
}
set print
# print $Data2
# convert matrix data into x y z data with empty lines inbetween
set print $Data3
do for [y=1:SizeY] {
do for [x=1:SizeX] {
print sprintf("%g %g %s", x, y, word($Data2[y],x))
print ""
}
}
set print
# print $Data3
set margins 0,0,0,0
unset colorbox
unset border
unset key
unset tics
set xrange[1:SizeX]
set yrange[1:SizeY]
plot $Data3 u 1:2:3 w l lw 1 lc rgb var notitle
set output
### end of code
Result: (100x100 pixels)
(enlarged with black background):
Image with 400x200 pixels (takes about 22 sec on my 8 year old laptop).
What I ended up actually using to get what I needed even though the question / bounty asks for a gnuplot solution:
matplotlib has a function matplotlib.pyplot.imsave which does what I was looking for... i.e. plotting 'just data pixels' and no extras like borders, margins, axes, etc. Originally I only knew about matplotlib.pyplot.imshow and had to pull a lot of tricks to eliminate all the extras from the image file and prevent any interpolation/smoothing etc (and therefore turned to gnuplot at a certain point). With imsave it's fairly easy, so I'm back to using matplotlib for an easy yet still flexible (in terms of colormap, scaling, etc) solution for 'pixel exact' plots. Here's an example:
#!/usr/bin/env python3
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
nx = 400
ny = 200
data = np.fromfile('T.dat', dtype=np.float32, count=nx*ny)
data = data.reshape((nx,ny), order='F')
matplotlib.image.imsave('T.png', np.transpose(data), origin='lower', format='png')
OK, here is another possible solution (I separated it from my first cumbersome approach). It creates the plot immediately, less than a second. No renaming necessary or creation of a useless file.
I guess key is to use term png and ps 0.1.
I don't have a proof but I think ps 1 would be ca. 6 pixels large and would create some overlap and/or white pixels at the corner. Again, for whatever reason it seems to work with term png but not with term pngcairo.
What I tested (Win7, gnuplot 5.2.6) is a binary file having the pattern 00 00 FF repeated all over (I can't display null bytes here). Since gnuplot apparently reads 4 bytes per array item (format="%d"), this leads to an alternating RGB pattern if I am plotting with lc rgb var.
In the same way (hopefully) we can figure out how to read format="%f" and use it together with a color palette. I guess that's what you are looking for, right?
Further test results, comments, improvements and explanations are welcome.
Code:
### pixel image from matrix data without strange white border
reset session
SizeX = 400
SizeY = 200
set terminal png size SizeX,SizeY
set output "tbPixelImage.png"
set margins 0,0,0,0
unset colorbox
unset border
unset key
unset tics
set xrange[0:SizeX-1]
set yrange[0:SizeY-1]
plot "tbBinary.dat" binary array=(SizeX,SizeY) format="%d" w p pt 5 ps 0.1 lc rgb var
### end of code
Result:

How to overlay two videos with blend filter in ffmpeg

I need to do a lot of videos with the next specifications:
A background video (bg.mp4)
Overlay a sequence of png images img1.png to img300.png (img%d.png) with a rate of 30 fps
Overlay a video with dust effects using a blend-lighten filter (dust.mp4)
Scale all the inputs to 1600x900 and if the input have not the aspect ratio, then crop them.
Specify the duration of the output-video to 10 sec (is the duration of image sequence at 30fps).
I've being doing a lot of test with different commands but always shows error.
Well, I think I got it in the next command:
ffmpeg -ss 00:00:18.300 -i music.mp3 -loop 1 -i bg.mp4 -i ac%d.png -i dust.mp4 -filter_complex "[1:0]scale=1600:ih*1200/iw, crop=1600:900[a];[a][2:0] overlay=0:0[b]; [3:0] scale=1600:ih*1600/iw, crop=1600:900,setsar=1[c]; [b][c] blend=all_mode='overlay':all_opacity=0.2" -shortest -y output.mp4
I'm going to explain in order to share what I've found:
Declaring the inputs:
ffmpeg -ss 00:00:18.300 -i music.mp3 -loop 1 -i bg.mp4 -i ac%d.png -i dust.mp4
Adding the filter complex. First part: [1,0] is the second element of the inputs (bg.mp4) and scaling to get the max values, and then cropping with the size I need, the result of this opperation, is in the [a] element.
[1:0]scale=1600:ih*1600/iw, crop=1600:900, setsar=1[a];
Second Part: Put the PNGs sequence over the resized video (bg.mp4, now [a]) and saving the resunt in the [b] element.
[a][2:0] overlay=0:0[b];
Scaling and cropping the fourth input (overlay.mp4) and saving in the [c] element.
[3:0]scale=1600:ih*1600/iw, crop=1600:900,setsar=1[c];
Mixing the first result with the overlay video with an "overlay" blending mode, and with an opacity of 0.1 because the video has gray tones and makes the result so dark.
[b][c] blend=all_mode='overlay':all_opacity=0.1
That's all.
If anyone can explay how this scaling filter works, I would thank a lot!
I needed to process a stack of images and was unable to get ffmpeg to work for me reliably, so I built a Python tool to help mediate the process:
#!/usr/bin/env python3
import functools
import numpy as np
import os
from PIL import Image, ImageChops, ImageFont, ImageDraw
import re
import sys
import multiprocessing
import time
def get_trim_box(image_name):
im = Image.open(image_name)
bg = Image.new(im.mode, im.size, im.getpixel((0,0)))
diff = ImageChops.difference(im, bg)
diff = ImageChops.add(diff, diff, 2.0, -100)
#The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. If the image is completely empty, this method returns None.
return diff.getbbox()
def rect_union(rect1, rect2):
left1, upper1, right1, lower1 = rect1
left2, upper2, right2, lower2 = rect2
return (
min(left1,left2),
min(upper1,upper2),
max(right1,right2),
max(lower1,lower2)
)
def blend_images(img1, img2, steps):
return [Image.blend(img1, img2, alpha) for alpha in np.linspace(0,1,steps)]
def make_blend_group(options):
print("Working on {0}+{1}".format(options["img1"], options["img2"]))
font = ImageFont.truetype(options["font"], size=options["fontsize"])
img1 = Image.open(options["img1"], mode='r').convert('RGB')
img2 = Image.open(options["img2"], mode='r').convert('RGB')
img1.crop(options["trimbox"])
img2.crop(options["trimbox"])
blends = blend_images(img1, img2, options["blend_steps"])
for i,img in enumerate(blends):
draw = ImageDraw.Draw(img)
draw.text(options["textloc"], options["text"], fill=options["fill"], font=font)
img.save(os.path.join(options["out_dir"],"out_{0:04}_{1:04}.png".format(options["blendnum"],i)))
if len(sys.argv)<3:
print("Syntax: {0} <Output Directory> <Images...>".format(sys.argv[0]))
sys.exit(-1)
out_dir = sys.argv[1]
image_names = sys.argv[2:]
pool = multiprocessing.Pool()
image_names = sorted(image_names)
image_names.append(image_names[0]) #So we can loop the animation
#Assumes image names are alphabetic with a UNIX timestamp mixed in.
image_times = [re.sub('[^0-9]','', x) for x in image_names]
image_times = [time.strftime('%Y-%m-%d (%a) %H:%M', time.localtime(int(x))) for x in image_times]
#Crop off the edges, assuming upper left pixel is representative of background color
print("Finding trim boxes...")
trimboxes = pool.map(get_trim_box, image_names)
trimboxes = [x for x in trimboxes if x is not None]
trimbox = functools.reduce(rect_union, trimboxes, trimboxes[0])
# #Put dates on images
testimage = Image.open(image_names[0])
font = ImageFont.truetype('DejaVuSans.ttf', size=90)
draw = ImageDraw.Draw(testimage)
tw, th = draw.textsize("2019-04-04 (Thu) 00:30", font)
tx, ty = (50, trimbox[3]-1.1*th) # starting position of the message
options = {
"blend_steps": 10,
"trimbox": trimbox,
"fill": (255,255,255),
"textloc": (tx,ty),
"out_dir": out_dir,
"font": 'DejaVuSans.ttf',
"fontsize": 90
}
#Generate pairs of images to blend
pairs = zip(image_names, image_names[1:])
#Tuple of (Image,Image,BlendGroup,Options)
pairs = [{**options, "img1": x[0], "img2": x[1], "blendnum": i, "text": image_times[i]} for i,x in enumerate(pairs)]
#Run in parallel
pool.map(make_blend_group, pairs)
This produces a series of images which can be made into a video like this:
ffmpeg -pattern_type glob -i "/z/out_*.png" -pix_fmt yuv420p -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -r 30 /z/out.mp4

Using LibRaw to correctly decode CR2 image?

My eventual goal is to decode CR2 images from multiple cameras for display in a desktop gui.
Using the LibRaw image decoding library, I've used the sample project to attempt to decode a CR2 image into a .TIFF file.
The original file as a jpg thumbnail is as follows:
Image 1
And the original CR2 after decoding and saving into a .TIFF is as follows:
Image 2
As you can see, the outcome is slightly brighter and yellowish.
The sample project was contains the following parameters for decoding images:
"-c float-num Set adjust maximum threshold (default 0.75)\n"
"-v Verbose: print progress messages (repeated -v will add verbosity)\n"
"-w Use camera white balance, if possible\n"
"-a Average the whole image for white balance\n"
"-A <x y w h> Average a grey box for white balance\n"
"-r <r g b g> Set custom white balance\n"
"+M/-M Use/don't use an embedded color matrix\n"
"-C <r b> Correct chromatic aberration\n"
"-P <file> Fix the dead pixels listed in this file\n"
"-K <file> Subtract dark frame (16-bit raw PGM)\n"
"-k <num> Set the darkness level\n"
"-S <num> Set the saturation level\n"
"-n <num> Set threshold for wavelet denoising\n"
"-H [0-9] Highlight mode (0=clip, 1=unclip, 2=blend, 3+=rebuild)\n"
"-t [0-7] Flip image (0=none, 3=180, 5=90CCW, 6=90CW)\n"
"-o [0-5] Output colorspace (raw,sRGB,Adobe,Wide,ProPhoto,XYZ)\n"
#ifndef NO_LCMS
"-o file Output ICC profile\n"
"-p file Camera input profile (use \'embed\' for embedded profile)\n"
#endif
"-j Don't stretch or rotate raw pixels\n"
"-W Don't automatically brighten the image\n"
"-b <num> Adjust brightness (default = 1.0)\n"
"-q N Set the interpolation quality:\n"
" 0 - linear, 1 - VNG, 2 - PPG, 3 - AHD, 4 - DCB\n"
#ifdef LIBRAW_DEMOSAIC_PACK_GPL2
" 5 - modified AHD,6 - AFD (5pass), 7 - VCD, 8 - VCD+AHD, 9 - LMMSE\n"
#endif
#ifdef LIBRAW_DEMOSAIC_PACK_GPL3
" 10-AMaZE\n"
#endif
"-h Half-size color image (twice as fast as \"-q 0\")\n"
"-f Interpolate RGGB as four colors\n"
"-m <num> Apply a 3x3 median filter to R-G and B-G\n"
"-s [0..N-1] Select one raw image from input file\n"
"-4 Linear 16-bit, same as \"-6 -W -g 1 1\n"
"-6 Write 16-bit linear instead of 8-bit with gamma\n"
"-g pow ts Set gamma curve to gamma pow and toe slope ts (default = 2.222 4.5)\n"
"-T Write TIFF instead of PPM\n"
"-G Use green_matching() filter\n"
"-B <x y w h> use cropbox\n"
"-F Use FILE I/O instead of streambuf API\n"
"-timing Detailed timing report\n"
"-fbdd N 0 - disable FBDD noise reduction (default), 1 - light FBDD, 2 - full\n"
"-dcbi N Number of extra DCD iterations (default - 0)\n"
"-dcbe DCB color enhance\n"
#ifdef LIBRAW_DEMOSAIC_PACK_GPL2
"-eeci EECI refine for mixed VCD/AHD (q=8)\n"
"-esmed N Number of edge-sensitive median filter passes (only if q=8)\n"
#endif
#ifdef LIBRAW_DEMOSAIC_PACK_GPL3
//"-amazeca Use AMaZE chromatic aberrations refine (only if q=10)\n"
"-acae <r b>Use chromatic aberrations correction\n" //modifJD
"-aline <l> reduction of line noise\n"
"-aclean <l c> clean CFA\n"
"-agreen <g> equilibrate green\n"
#endif
"-aexpo <e p> exposure correction\n"
// WF
"-dbnd <r g b g> debanding\n"
#ifndef WIN32
"-mmap Use mmap()-ed buffer instead of plain FILE I/O\n"
#endif
"-mem Use memory buffer instead of FILE I/O\n"
"-disars Do not use RawSpeed library\n"
"-disinterp Do not run interpolation step\n"
"-dsrawrgb1 Disable YCbCr to RGB conversion for sRAW (Cb/Cr interpolation enabled)\n"
"-dsrawrgb2 Disable YCbCr to RGB conversion for sRAW (Cb/Cr interpolation disabled)\n"
"-disadcf Do not use dcraw Foveon code either if compiled with demosaic-pack-GPL2\n"
I've tried various options to replicate the image in the thumbnail, such as white balancing (-w), interpolation quality (-q N), and embedded color matrix (+M). When I used white balancing, it removed the yellowish tint but produced a bright image. I then went on to disable automatic brightening (-W) and it produced a non-yellow image but much darker than the thumbnail.
What image decoding parameters will help me to decode the CR2 into the a high-quality image that looks like the thumbnail (in terms of color, brightness, etc.)?
Problem solved.
While I don't have the specifics just yet, my senior said windows didn't support the original formatting option, and he also moved memory row-by-by.

Image::Magick (perlmagick) resizing aspect ratio and quality issues (different than convert command line utility)

I am attempting to do some bulk resizing operations of images using ImageMagick and perlmagick (Image::Magick). All of the images I have as sources are large images and I want to resize them down to various intervals or either height or width. I want to always preserve the aspect ratio.
Given an example image with dimensions of 3840 pixels × 2160 pixels (3840x2160) I want to create the following resized images:
?x1000
?x500
?x100
1600x?
1200x?
800x?
400x?
I can do this very simply using the convert command line utility with the following commands (in order):
convert input_filename.jpg -resize x1000 output_wx1000.jpg
convert input_filename.jpg -resize x500 output_wx500.jpg
convert input_filename.jpg -resize x100 output_wx100.jpg
convert input_filename.jpg -resize 1600 output_1600xh.jpg
convert input_filename.jpg -resize 1200 output_1200xh.jpg
convert input_filename.jpg -resize 800 output_800xh.jpg
convert input_filename.jpg -resize 400 output_400xh.jpg
Since I am attempting to perform these operations in bulk in conjunction with other operations I am attempting to perform these same operations in perl using Image::Magick. I have tried several different methods with the following results:
#METHOD 1
my $image = Image::Magick->new();
$image->Read($input_filename);
$image->Resize(
($width ? ('width' => $width) : ()),
($height ? ('height' => $height) : ()),
);
$image->Write(filename => $output_filename);
This results in images that do not maintain aspect ratio. For example, if a height of 100 is supplied, the output image will be the original width by 100 (3840x100). A comparable effect is had when supplying a width -- the height is maintained, but the aspect ratio is not.
#METHOD 2
my $image = Image::Magick->new();
$image->Read($input_filename);
die "Only one dimension can be supplied" if $width && $height;
$image->Resize(geometry => $width) if $width;
$image->Resize(geometry => "x$height") if $height;
$image->Write(filename => $output_filename);
This results in images that maintain aspect ratio, and if the geometry operation is based on height, the output is exactly what is intended. However, if a width is supplied the output is terribly blurry.
#METHOD 3
`convert "$input_filename" -resize $width "$output_filename"` if $width;
`convert "$input_filename" -resize x$height "$output_filename"` if $height;
This results in images that are all correct, but forks outside of the perl process leading to efficiency issues.
Is there a better way in perl to make this resize operation produce the same results as the command-line convert utility?
My command line utility reports version 6.7.9-10, and Image::Magick reports version 6.79.
Your method #2 is on the right track. To preserve aspect ratio, supply the width and height via the geometry keyword. Your procedure can be made more general by performing the resize in one call instead of two:
$image->Resize(geometry => "${width}x${height}");
This ensures that Resize will only be called once, even if you supply both $width and $height. Just make sure that if either value is not supplied, you set it to the empty string. If you supplied both a width and height to your procedure in method #2, that could have been the cause of the blurriness you saw.
Another possible source of blurriness is the filter used by the resize operator. The best filter to use for a given operation depends on both the color characteristics of the image and the relationship between the original dimensions and the target dimensions. I recommend reading through http://www.imagemagick.org/script/command-line-options.php#filter for information about that. In PerlMagick, you can specify the filter for Resize to use via the filter keyword.
That said, I did not find particular problems with blurriness with images that I tried, so if the problem persists, a test image would be most helpful.
I might be a little late to this party, but as I had a very similar goal - resizing an image and maintaining a balance between image quality and the amount of disc space it takes up - I came up with the following code. I started out with OPs code and followed this very interesting article: https://www.smashingmagazine.com/2015/06/efficient-image-resizing-with-imagemagick/
This is the result:
sub optimize_image_size
{
my $imagePath = shift();
my $height = shift(); #720
my $width = shift(); #1080
my $image = Image::Magick->new();
$image->Read($imagePath);
die "Only one dimension can be supplied" if $width && $height;
$image->Thumbnail(geometry => "$width",filter=>'Triangle') if $width;
$image->Thumbnail(geometry => "x$height",filter=>'Triangle') if $height;
$image->Colorspace(colorspace=>'sRGB');
$image->Posterize(levels=>136, dither=>'false');
$image->UnsharpMask(radius=>0.25, sigma=>0.25, threshold=>0.065, gain=>8);
$image->Write(filename => $imagePath
, quality=>'82'
, interlace=>'None'
);
}
At least for me it produces very satisfactory size reduction (my 6MB sample images were reduced to about 90Kb), while keeping a quality similar to Photoshops "for web" settings and of course maintaining aspect ratio no matter if you provide width or height.
Too late for OP but maybe it helps other people.

How to change the print size of an image in mm on command line?

ImageMagick seems to be best to convert image files on the command line. However it only supports changing the size in pixels and the resolution in inch per pixel but not the print size (as shown with the command identify -verbose). I'd like to:
quickly get the image print size in mm
change the image print size (by setting either height or width or both to a new value in mm)
This should be able with simple shell scripting, shouldn't it?
The only absolute dimension for images are pixels.
Resolution, mm or density or resolution do only come into play when you render the image on a certain surface (screen display, paper printout).
These have their own built-in, hardware-dependent resolution. If you know it, you can compute the mm values for the image dimensions, provided you want to render it in its "natural size".
Very often you do not want the "natural size" -- sometimes you may want: "fill the Letter-sized paper with the image" (scale to fit). If that happens, the same image will have to be scaled up or down -- but the screen or printer resolution will not change, it's only that an interpolation algorithm will start to add pixels to fill the gap (scale up) or remove pixels to make the picture appear smaller (scale down).
So before someone can give an algorithm about how to compute the "image size in mm" (in the image's natural size) you need to know the resolution of the target device (screen or printer).
Edit:
If you embed a given image (which has its size in pixels) into a PDF (where the source document comes for example from LaTeX), you still have to specify...
...either at which resolution you want the image be rendered on the page
...or at which size (either in mm or in % of the page dimensions) you want the image rendered.
You cannot determine both these parameters at the same time without resampling the image. Pick one, and the other is implicitly determined by your pick.
To give an example.
Assume your original image is 2160x1440 pixels.
Your LaTeX -> PDF transformation is done by Ghostscript. Ghostscript internally uses a default resolution of 720 dpi for all raster objects. So unless you set "resolution" explicitly to a different value for your PDF conversion, the image will have a size of 3x2 inches (76.2 x 50.8 mm) on a PDF or print page.
If you set the resolution to 90 dpi, the image will have a size of 24x16 inches (609.6 x 406.4 mm) on the page.
If you set the resolution to 270 dpi (which is close to the commonly used 300 dpi), the image size transforms to 8x5.333 inches (203.2 x 135.5 mm).
So the formula for a shell script is:
# 25.4 mm == 1 inch
image_width_px=W # in pixels (integer)
image_height_px=H # in pixels (integer)
resolution=R # in pixels/inch
image_width_in_inch=$((W / R)) # Shell arithmetics: does only handle
image_height_in_inch=$((H / R)) #+ and return integers!
image_width_in_mm=$(( $((W / R)) * 254/10 ))
image_height_in_mm=$(( $((H / R)) * 254/10 ))
# use 'bc' to achieve higher precision arithmetics:
precise_image_width_in_mm=$( echo \
"$image_width_px / $resolution * 25.4" \
| bc -l )
precise_image_height_in_mm=$( echo \
"$image_height_px / $resolution * 25.4" \
| bc -l )
I tried to solve it with my own script in Perl. One must calculate the dots per inch based on the size in pixels and the requested print size, as explained by Kurt Pfeilfe in his answer.
sub getsize {
my $file = shift;
my $info = do { # avoid the shell, no escaping needed
my #args = ('-format','%G %x%y','-units','PixelsPerInch',$file);
open my $fh, "-|", "identify", #args or die "$!\n";
<$fh>;
};
if ($info =~ /^(\d+)x(\d+) (\d+) PixelsPerInch(\d+) PixelsPerInch/) {
my ($px,$py,$dx,$dy) = ($1,$2,$3,$4);
my ($sx,$sy) = map { $_ * 25.4 } ($px/$dx, $py/$dy);
return ($px,$py,$dx,$dy,$sx,$sy);
} else {
die $info;
}
}
foreach my $file (#ARGV) {
if ($file =~ /^(\d*)(x(\d+))?mm$/) {
($mx,$my) = ($1,$3);
} elsif( -e $file ) {
my ($w,$h);
if ($mx || $my) {
my ($px,$py,$dx,$dy,$sx,$sy) = getsize($file);
my $rx = 25.4 * ( $mx ? ($px/$mx) : ($py/$my) );
my $ry = 25.4 * ( $my ? ($py/$my) : ($px/$mx) );
system qw(convert -units PixelsPerInch -density),
sprintf("%.0fx%.0f",$rx,$ry), $file, $file;
}
printf "$file: %dx%d at %dx%ddpi = %dx%dmm", getsize($file);
} else {
die "file not found: $file\n";
}
}
The script does not support fractions of milimeters, feel free to modify the source.

Resources