/*
* Sinc-based image resampler (fast version)
*
* Copyright (c) 2020 Project Nayuki
* https://www.nayuki.io/page/sinc-based-image-resampler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program (see COPYING.txt).
* If not, see
Usage: java FastSincImageResample InFile.{png,bmp} OutWidth OutHeight OutFile.{png,bmp} [HorzFilterLen [VertFilterLen]]
After calling {@code run()}, it is recommend that this object should not be reused for another resampling operation. * This is because various fields probably need to be cleared, such as the filter length and output file type.
* @throws IOException if an I/O exception occurred * @throws IllegalStateException if there is no input image or the output dimensions are not set to positive values */ public void run() throws IOException { // Read input file (optional) if (inputFile != null) { try { inputImage = ImageIO.read(inputFile); } catch (IOException e) { throw new IOException("Error reading input file (" + inputFile + "): " + e.getMessage(), e); } } // Get input image dimensions if (inputImage == null) throw new IllegalStateException("No input image"); inputWidth = inputImage.getWidth(); inputHeight = inputImage.getHeight(); // Calculate filter lengths (optional) if (outputWidth <= 0 || outputHeight <= 0) throw new IllegalStateException("Output dimensions not set"); if (horizontalFilterLength <= 0) horizontalFilterLength = Math.max((double)inputWidth / outputWidth, 1) * 4.0; if (verticalFilterLength <= 0) verticalFilterLength = Math.max((double)inputHeight / outputHeight, 1) * 4.0; // Resample the image if (threads <= 0) threads = Runtime.getRuntime().availableProcessors(); resampleImage(); // Write output file (optional) if (outputFile != null) { if (outputFileType == null) { // Auto-detection by file extension String lowername = outputFile.getName().toLowerCase(); if (lowername.endsWith(".bmp")) outputFileType = "bmp"; else outputFileType = "png"; // Default } try { ImageIO.write(outputImage, outputFileType, outputFile); } catch (IOException e) { throw new IOException("Error writing output file (" + outputFile + "): " + e.getMessage(), e); } } } private void resampleImage() { final int inWidth = inputWidth; final int inHeight = inputHeight; final int outWidth = outputWidth; final int outHeight = outputHeight; // Get packed int pixels int[] inPixels = new int[inWidth * inHeight]; inputImage.getRGB(0, 0, inWidth, inHeight, inPixels, 0, inWidth); // Convert to float final float[] inVert = new float[inWidth * inHeight * 3]; for (int i = 0, j = 0; i < inPixels.length; i++, j += 3) { int rgb = inPixels[i]; inVert[j + 0] = (rgb >>> 16) & 0xFF; inVert[j + 1] = (rgb >>> 8) & 0xFF; inVert[j + 2] = (rgb >>> 0) & 0xFF; } inPixels = null; Thread[] thr = new Thread[threads]; final AtomicInteger sharedY = new AtomicInteger(0); final float[] inHorz = new float[inWidth * outHeight * 3]; final CyclicBarrier barrier = new CyclicBarrier(threads); final AtomicInteger sharedX = new AtomicInteger(0); final int[] outPixels = new int[outWidth * outHeight]; for (int i = 0; i < thr.length; i++) { thr[i] = new Thread() { public void run() { // Resample vertically and transpose { double sincScale = Math.min((double)outHeight / inHeight, 1); double[] weights = new double[(int)verticalFilterLength + 1]; float[] outVertRow = new float[inWidth * 3]; while (true) { // For each output row int y = sharedY.getAndIncrement(); if (y >= outHeight) break; double weightSum = 0; double centerY = (y + 0.5) / outHeight * inHeight; // In input image coordinates double filterStartY = centerY - verticalFilterLength / 2; int startIndex = (int)Math.ceil(filterStartY - 0.5); for (int i = 0; i < weights.length; i++) { int inputY = startIndex + i; double weight = windowedSinc((inputY + 0.5 - centerY) * sincScale, (inputY + 0.5 - filterStartY) / verticalFilterLength); weights[i] = weight; weightSum += weight; } Arrays.fill(outVertRow, 0); for (int i = 0; i < weights.length; i++) { double weight = weights[i] / weightSum; int clippedInputY = Math.min(Math.max(startIndex + i, 0), inHeight - 1); for (int x = 0; x < inWidth; x++) { // For each pixel in the row int j = (clippedInputY * inWidth + x) * 3; outVertRow[x * 3 + 0] += inVert[j + 0] * weight; outVertRow[x * 3 + 1] += inVert[j + 1] * weight; outVertRow[x * 3 + 2] += inVert[j + 2] * weight; } } for (int x = 0; x < inWidth; x++) { int j = (x * outHeight + y) * 3; inHorz[j + 0] = outVertRow[x * 3 + 0]; inHorz[j + 1] = outVertRow[x * 3 + 1]; inHorz[j + 2] = outVertRow[x * 3 + 2]; } } } // Wait for all threads to finish the phase try { barrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } // Resample horizontally and transpose { double sincScale = Math.min((double)outWidth / inWidth, 1); double[] weights = new double[(int)horizontalFilterLength + 1]; double[] outHorzCol = new double[outHeight * 3]; while (true) { // For each output column int x = sharedX.getAndIncrement(); if (x >= outWidth) break; double weightSum = 0; double centerX = (x + 0.5) / outWidth * inWidth; // In input image coordinates double filterStartX = centerX - horizontalFilterLength / 2; int startIndex = (int)Math.ceil(filterStartX - 0.5); for (int i = 0; i < weights.length; i++) { int inputX = startIndex + i; double weight = windowedSinc((inputX + 0.5 - centerX) * sincScale, (inputX + 0.5 - filterStartX) / horizontalFilterLength); weights[i] = weight; weightSum += weight; } Arrays.fill(outHorzCol, 0); for (int i = 0; i < weights.length; i++) { double weight = weights[i] / weightSum; int clippedInputX = Math.min(Math.max(startIndex + i, 0), inWidth - 1); for (int y = 0; y < outHeight; y++) { // For each pixel in the column int j = (clippedInputX * outHeight + y) * 3; outHorzCol[y * 3 + 0] += inHorz[j + 0] * weight; outHorzCol[y * 3 + 1] += inHorz[j + 1] * weight; outHorzCol[y * 3 + 2] += inHorz[j + 2] * weight; } } for (int y = 0; y < outHeight; y++) { // Convert to 8 bits per channel and pack integers double r = outHorzCol[y * 3 + 0]; if (r < 0) r = 0; if (r > 255) r = 255; double g = outHorzCol[y * 3 + 1]; if (g < 0) g = 0; if (g > 255) g = 255; double b = outHorzCol[y * 3 + 2]; if (b < 0) b = 0; if (b > 255) b = 255; outPixels[y * outWidth + x] = (int)(r + 0.5) << 16 | (int)(g + 0.5) << 8 | (int)(b + 0.5); } } } } }; thr[i].start(); } try { for (Thread th : thr) th.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } outputImage = new BufferedImage(outWidth, outHeight, BufferedImage.TYPE_INT_RGB); outputImage.setRGB(0, 0, outWidth, outHeight, outPixels, 0, outWidth); } // x is measured in half-cycles; y is for the window which has the domain [0, 1] private static double windowedSinc(double x, double y) { x *= Math.PI; double sinc = x != 0 ? Math.sin(x) / x : 1; double window = 0 <= y && y <= 1 ? 1 - Math.abs(y - 0.5) * 2 : 0; // Triangle window return sinc * window; } }