This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
olie304's profile picture

Image to Terminal glasses efficiency

Started by olie304, 05 November 2017 - 06:32 PM
olie304 #1
Posted 05 November 2017 - 07:32 PM
Hello everyone, I made a small script in Java that converts any image to a program that can be ran with Terminal Glasses from Open Peripherals. It works great however I have encountered some issues: It takes a while to load the image and causes a lag spike whilst doing so, The program can crash people who use a large enough image (or smaller ones that are 480x270 in some cases) if they are running a potato, and the files are immensely large.

To combat these issues I have tried: Making a color palette and using a loop to create each pixel which resulted in a longer load time, Making a list of colors and then a loop which increased the file size and made the load longer, Making the script produce multiple volumes that get taken off my server so the program could fit on a computer which works but it isn't optimal, and I have tried cutting out as much fat as possible by making similar pixels on the X axis one block and by using functions to minimize the length of repeated variables and methods to one character; this has also helped a lot.

Is there anything I am missing? At the very least I want to make a system for similar pixels on the Y axis to be turned into one block but I do not know how to approach this correctly, and if I am a little ambitious it would be good to have the output fit into one or two files.

Thanks.

Code:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

public class ImageToGlasses {

public static void main(String args[]) throws IOException {

//Horizontal size of a box with the same pixel colors
  int boxLeng = 1;

//Should it reboot once done writing to peripheral?
  boolean startupProgram = false;

//Current line of output file, resets every 21600 lines
  int currentLine = 0;

//Keep scanning down X for similar pixel color?
  boolean keepGoing = true;

//Previous color down X to test similar color
  int prevColor = 0;

//Number of files created
  int fileNo = 0;

//Output program
  String outputName = "outFile";

//Input PNG or JPG
  String fileName = "inImage.png";


  if(args.length == 1) {
   outputName = args[1];
   fileName = args[0];
  }else{
   System.out.println("Ex: java -jar ImageToGlasses.jar image.png outputLua");
  }


  File file = new File(fileName);
  BufferedImage image = ImageIO.read(file);

  Files.deleteIfExists(Paths.get(outputName));

  BufferedWriter writer = new BufferedWriter(new FileWriter(outputName, true));

//Write a header
  writer.append("g = peripheral.wrap(\"top\")\n");
  writer.append("function b(x,y,leng,hex,opacity)\n");
  writer.append("g.addBox(x,y,leng,1,hex,opacity)\n");
  writer.append("end\n");
  writer.append("g.clear()\n");

//Scan Left to right; Up to Down
  for (int y = 0; y < image.getHeight(); y++) {
   for (int x = 0; x < image.getWidth(); x++) {

	int clr = image.getRGB(x, y);
	int red = (clr &amp; 0x00ff0000) >> 16;
	int green = (clr &amp; 0x0000ff00) >> 8;
	int blue = clr &amp; 0x000000ff;
	int alpha = (clr &amp; 0xff000000) >>> 24;
	String hex = String.format("0x%02X%02X%02X", red, green, blue);

//Is the last pixel down X the same as the current one? If so extend length of box
	if (prevColor == clr) {

	 keepGoing = true;
	 for (int ex = x; ex < image.getWidth() &amp;&amp; keepGoing; ex++) {

	  if (image.getRGB(ex, y) == prevColor) {

	   boxLeng++;

	  } else {

	   keepGoing = false;

	  }
	 }
	}

	prevColor = clr;

//Use opacity if applicable, Prevents the writing of fully transparent pixels to save space
	if (((Integer) alpha).doubleValue() / 255.0 > 0.0) {

	 writer.append("b(" + x + "," + y + "," + boxLeng + "," + hex + "," + (((Integer) alpha).doubleValue() / 255.0) + ")\n");

	}
  
//When there are similar pixels start scanning X after the box
	if (boxLeng > 1) {

	 x = x + boxLeng - 1;

	}

	boxLeng = 1;

//Limit the file size, checks every 21600 lines to see if it's larger than ~500KB
	if (currentLine == 21600) {

	 boolean createNewFile = false;
	 currentLine = 0;
	 writer.close();

	 if (fileNo == 0) {

	  if (Files.size(Paths.get(outputName)) >= 300000) {

	   createNewFile = true;
	   writer = new BufferedWriter(new FileWriter(outputName, true));

	  } else {

	   writer = new BufferedWriter(new FileWriter(outputName, true));

	  }

	 } else {

	  if (Files.size(Paths.get(outputName + fileNo)) >= 300000) {

	   createNewFile = true;
	   writer = new BufferedWriter(new FileWriter(outputName + fileNo, true));

	  } else {

	   writer = new BufferedWriter(new FileWriter(outputName + fileNo, true));

	  }
	 }
	
//Check if file exceeded max size, closes writer and starts a new one
	 if (createNewFile) {

	  fileNo++;
	  writer.append("shell.run(\"paster run " + outputName + fileNo + "\")");
	  writer.close();
	  Files.deleteIfExists(Paths.get(outputName + fileNo));
	  writer = new BufferedWriter(new FileWriter(outputName + fileNo, true));

	  writer.append("g = peripheral.wrap(\"top\")\n");
	  writer.append("function b(x,y,leng,hex,opacity)\n");
	  writer.append("g.addBox(x,y,leng,1,hex,opacity)\n");
	  writer.append("end\n");

	 }
	}

	currentLine++;

   }
  }

//Add footer when done with current file
  writer.append("g.sync()\n");

  if (startupProgram) {
   writer.append("shell.run(\"startup\")");
  }

  writer.close();

}
}
Luca_S #2
Posted 05 November 2017 - 07:46 PM
Just took a glance over it, but here is the first thing I would change:

if (((Integer) alpha).doubleValue() / 255.0 > 0.0) {
Why are you dividing by 255 here? Considering you are only checking if it is above 0 "alpha > 0" should do just fine and will also prevent casting your int to an Integer and calling the oubleValue() method.
Exerro #3
Posted 05 November 2017 - 09:05 PM
In your header, you have this code:

g = peripheral.wrap("top")
function b(x,y,leng,hex,opacity)
	g.addBox(x,y,leng,1,hex,opacity)
end
then call the function like

b(a,b,c,d,e)

From my own testing, you can increase the performance 3x by simply changing the code to this:
Not sure how this happened but running the exact same program since shows only minor performance increases, as I was expecting initially. I'm guessing some kind of emulator bug? Either way, this will be more efficient, but only a little. (I'm genuinely so confused about the time differences, it's not even close to minor)

local g = peripheral.wrap("top")
local b = g.addBox
Then calling

b(a,b,c,1,d,e)
Note that this effect is exaggerated the less time g.addBox takes. With just 1 call to `type{}` (see code below), the difference jumps up to 1.5x for locals.

Spoiler


local function printf( fmt, ... )
	return print( fmt:format( ... ) )
end

local function timef( f, c, ... )
	local t = ccemux and ccemux.milliTime() or os.clock()
	for i = 1, c do
		f( ... )
	end
	return ccemux and (ccemux.milliTime() - t) / 1000 or os.clock() - t
end

g = {
	--# non-trivial function
	addBox = function() for i = 1, 100 do type{} end end
}

local count = 5000
local localAddBox = g.addBox

function b(x,y,leng,hex,opacity)
	g.addBox(x,y,leng,1,hex,opacity)
end

local timeGlobals = timef(b, count, 1, 2, 3, 4, 5)
local timeLocals = timef(localAddBox, count, 1, 2, 3, 100, 4, 5)

printf("Time for globals: %s", timeGlobals)
printf("Time for locals: %s", timeLocals)
printf("Performance ratio: %s", timeGlobals / timeLocals)


Seeing an example output file would help.
Edited on 05 November 2017 - 08:20 PM
olie304 #4
Posted 05 November 2017 - 11:11 PM
Thanks guys, I will be trying out some of those things soon. Here is an example output http://auraxium.com/Examples/ (they exceed 500k so I have them on my site)
Bomb Bloke #5
Posted 06 November 2017 - 03:04 AM
When searching for duped pixels, it'd be easier to look at the next color for matches, not the previous colour. If you really want to check the previous colour then you'll need to jump through a few hoops in order to avoid generating stuff like this within your output:

b(22,207,1,0x010101,1.0)
b(23,207,3,0x010101,1.0)

Checking for duped pixels in two dimensions might be a bit tricky, but it may not be too painful to perform an initial count-up to find the "most common colour" before you do your main draw. That colour can then be plonked down as one big box (assuming it doesn't turn out to be "full transparency"), effectively making it a "background".

You could stand to drop the precision on your alpha values a bit (just round to the nearest hundredth), and there's certainly no call for any "point zeroes". You could also ignore the lowest bit of your R/G/B values when doing comparisons - this probably wouldn't reduce visual quality by any noticeable amount, but it'd potentially save a lot of space.

The largest reduction would be to make your code just a little more complex. Instead of generating a million lines of function calls with nearly identical x/y co-ords baked into them, generate a table with just the raw data you need, then use a Lua loop structure to do the actual drawing. Eg:

Spoiler
local glasses = peripheral.wrap("top")
local box = glasses.addBox

local myImgData = {
	{colStart, rowStart, {len1, col1, alpha1}, {len2, col2, alpha2}, etc},
	{colStart, rowStart, {len1, col1, alpha1}, {len2, col2, alpha2}, etc},
	etc
}

for i = 1, #myImgData do
	local thisLine = myImgData[i]
	local x, y = thisLine[1], thisLine[2]
	
	for i = 3, #thisLine do
		local thisBox = thisLine[i]
		if thisBox[3] > 0 then box(x, y, thisBox[1], 1, thisBox[2], thisBox[3]) end
		x = x + thisBox[1]
	end
end

Really I don't see any great reason to duplicate the "executable code" within every single "data file" you produce, though. You could again get another large reduction in filesize by switching to a "binary" output format and keeping your "image drawing" script code in a single stand-alone file.

BBPack may be useful here. Entering "bbpack compress" at the command line, and then adding os.loadAPI("bbpack") to the top of your startup file, will typically allow you to store a lot more on your computers. You can likewise do "bbpack <URL> <localPath>" to create links to online sources from within your local filesystem, making it easy to work with files which're potentially much larger than what your drives could hold.
olie304 #6
Posted 08 November 2017 - 12:53 AM
When searching for duped pixels, it'd be easier to look at the next color for matches, not the previous colour. If you really want to check the previous colour then you'll need to jump through a few hoops in order to avoid generating stuff like this within your output:

b(22,207,1,0x010101,1.0)
b(23,207,3,0x010101,1.0)

Checking for duped pixels in two dimensions might be a bit tricky, but it may not be too painful to perform an initial count-up to find the "most common colour" before you do your main draw. That colour can then be plonked down as one big box (assuming it doesn't turn out to be "full transparency"), effectively making it a "background".

You could stand to drop the precision on your alpha values a bit (just round to the nearest hundredth), and there's certainly no call for any "point zeroes". You could also ignore the lowest bit of your R/G/B values when doing comparisons - this probably wouldn't reduce visual quality by any noticeable amount, but it'd potentially save a lot of space.

The largest reduction would be to make your code just a little more complex. Instead of generating a million lines of function calls with nearly identical x/y co-ords baked into them, generate a table with just the raw data you need, then use a Lua loop structure to do the actual drawing. Eg:

Spoiler
local glasses = peripheral.wrap("top")
local box = glasses.addBox

local myImgData = {
	{colStart, rowStart, {len1, col1, alpha1}, {len2, col2, alpha2}, etc},
	{colStart, rowStart, {len1, col1, alpha1}, {len2, col2, alpha2}, etc},
	etc
}

for i = 1, #myImgData do
	local thisLine = myImgData[i]
	local x, y = thisLine[1], thisLine[2]
	
	for i = 3, #thisLine do
		local thisBox = thisLine[i]
		if thisBox[3] > 0 then box(x, y, thisBox[1], 1, thisBox[2], thisBox[3]) end
		x = x + thisBox[1]
	end
end

Really I don't see any great reason to duplicate the "executable code" within every single "data file" you produce, though. You could again get another large reduction in filesize by switching to a "binary" output format and keeping your "image drawing" script code in a single stand-alone file.

BBPack may be useful here. Entering "bbpack compress" at the command line, and then adding os.loadAPI("bbpack") to the top of your startup file, will typically allow you to store a lot more on your computers. You can likewise do "bbpack <URL> <localPath>" to create links to online sources from within your local filesystem, making it easy to work with files which're potentially much larger than what your drives could hold.

Using bbpack is a good idea, it has solved a lot of my problems in the past. I am afraid of using a loop or reading a raw binary image because of how slow it will be. From previous attempts it took up to 30 minutes to load a 480p image that can be loaded in 15 seconds using a jillion billion method calls.
Bomb Bloke #7
Posted 08 November 2017 - 05:51 AM
That sounds… unlikely. I mean, yeah, direct calls will technically execute faster than ones requiring table lookups… but 120x faster? No, I'd say there was something very wrong with your implementation.
olie304 #8
Posted 12 November 2017 - 11:10 PM
That sounds… unlikely. I mean, yeah, direct calls will technically execute faster than ones requiring table lookups… but 120x faster? No, I'd say there was something very wrong with your implementation.
Seems like you are right, using nested arrays for this works way better, however I am having a difficult time making the final image not distorted. It seems that when it deals with a transparent file, since I do not include the fully transparent pixels to save space, it ends up placing everything that is on the same line right next to each other. I have tried accounting for the transparent pixels by adding an extra length element, but nothing I do seems to work correctly: either everything gets moved or nothing gets moved. I have been trying to count up the amount of spaces with clear pixels in-between each colored one and then add it to the X value along with the box size.

It should look like this: (image), however without changing anything it looks like this: (image) (output code), and if I add a counter to add a space between two colored pixels it looks like this: (image). It works fine if I re-include the transparent pixels but of course this isn't optimal. Any idea on what's happening here?
Bomb Bloke #9
Posted 13 November 2017 - 06:33 AM
The image data you've generated seems to be rendering correctly. The issue is in your (omitted) Java code: it's setting box[4] to 0 for each and every record.

(In cases where it should be set to 0, I personally wouldn't define it at all.)

Your duped pixel checker is also still broken, there are a lot of savings to be made by fixing that!
olie304 #10
Posted 13 November 2017 - 11:08 PM
The image data you've generated seems to be rendering correctly. The issue is in your (omitted) Java code: it's setting box[4] to 0 for each and every record.

(In cases where it should be set to 0, I personally wouldn't define it at all.)

Your duped pixel checker is also still broken, there are a lot of savings to be made by fixing that!
Sorry if I didn't clear this up, I am aware of the other bugs which I plan on fixing but I am having a lot of trouble with a new issue. I have worked on it a bit but the only fix I can make is an output line like this

{0,187,{1,0xFF5000},{53,0xFF5000},{1,0xFF5000,0.2},{1},{106},{1,0x00AEEF},{145,0x00AEEF},{1},{169},{1,0x00FF00},{1,0x00FF00}},
English translation: Start line at X=0,Y=187. Add 1 colored box with a length of 0 then one after with a length of 52 then another after with a length of 1. Next add an empty space after for 106 pixels. E.t.c.
From above, I mentioned the reason box[4] existed was to make up for the transparent pixels. In the example line from this post I have omitted box[4] entirely just added the location a box of transparent pixels starts: like {169} which has no other data. The lua now does this:

if #box == 3 then
b(x, y, box[1], box[2], box[3])
elseif(#box == 2) then
b(x, y, box[1], box[2], 1.0) -- Idea from calling out the repetitive 0s. Now every, very common, fully colored pixel no longer needs to be defined.
end
x = x + box[1]
My problem is that whenever I try to add the length of the transparent pixels to another piece of data it should read (i.e. box[4]) then it distorts even more.

p.s. I disabled the code that assigns box[4] in the example you saw so I could create the two example files.
Edited on 13 November 2017 - 10:28 PM
Bomb Bloke #11
Posted 15 November 2017 - 04:17 AM
This:

if #box == 3 then
b(x, y, box[1], box[2], box[3])
elseif(#box == 2) then
b(x, y, box[1], box[2], 1.0) -- Idea from calling out the repetitive 0s. Now every, very common, fully colored pixel no longer needs to be defined.
end

… can be simplified to this:

b(x, y, box[1], box[2], box[3] or 1)

I really can't comment further on your transparency issues without seeing all of the code involved. I can tell you the "English translation" you've got there doesn't seem to match the numbers in the line above it, though.
olie304 #12
Posted 10 April 2018 - 02:48 AM
Alright, I know it has been a while, forgive me. I have made a pretty solid format using the following:

File file = new File(fileName);
BufferedImage image = ImageIO.read(file);

Files.deleteIfExists(Paths.get(outputName));
FileOutputStream fileOut = new FileOutputStream(outputName);
BufferedOutputStream writer = new BufferedOutputStream(fileOut);

writer.write(1); //1 Volume (Future Implementation)
writer.write(image.getHeight() &amp;  255);
writer.write((image.getHeight() >> 8) &amp; 255);
writer.write(image.getWidth() &amp; 255);
writer.write((image.getWidth() >> 8) &amp; 255);

int[][] lengths = new int[image.getHeight()][image.getWidth()];
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
  int leng = 1;
  while (x + leng < image.getWidth() &amp;&amp; image.getRGB(x, y) == image.getRGB(x + leng, y)) {
   leng++;
  }
  lengths[y][x] = leng;
  x += leng - 1;
}
}

//Scan Left to right; Up to Down
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
  int clr = image.getRGB(x, y);
  int red = (clr &amp; 0x00ff0000) >> 16;
  int green = (clr &amp; 0x0000ff00) >> 8;
  int blue = clr &amp; 0x000000ff;
  int alpha = (clr &amp; 0xff000000) >>> 24;

  writer.write(lengths[y][x] &amp; 255);
  writer.write((lengths[y][x] >> 8) &amp;  255);
  writer.write(red);
  writer.write(green);
  writer.write(blue);
  writer.write(alpha);
}
}
And the Lua:

local fileName = "input"
-- Glasses Bridge Side
local side = "top"
local g = peripheral.wrap(side)
local lastTime = os.clock()
local queueEvent = os.queueEvent
local pullEvent = os.pullEvent
local myEvent
local addBox = g.addBox
local sync = g.sync
local function b(x,y,leng,hex,opacity)
  addBox(x,y,leng,1,hex,opacity)
end
g.clear()
local input = fs.open(fileName, "rb")
local read = input.read
volumes = read()
local height = read() + read() * 256
local width = read() + read() * 256
local heighty
local widthy
for heighty=0, height-1 do
  for widthy=0, width-1 do
	--local r,g,b,a = input.read(),input.read(),input.read(),input.read()
	b(widthy,heighty,read() + read() * 256,((read()*65536)+(read()*256))+read(),read()*0.00392156863)
	myEvent = tostring({})
	queueEvent(myEvent)
	pullEvent(myEvent)
  end
end
print("T: "..os.clock()-lastTime) --Takes 53.3 seconds using a moderately intensive image
input.close()
g.sync()
My only issue now is that it is still super slow. I skimmed through this handy guide and took anything that could apply to JLua but it still isn't that quick. Is it even possible to go faster?
Edited on 10 April 2018 - 12:57 AM