Pointers pixmapa

Started by Ryszard, March 11, 2021, 15:30:49

Previous topic - Next topic

Ryszard

Good morning!

I am scanning technical documentation for archiving purposes. From the scanner I have tify - 8 bit - grayscale. The files are sometimes very large. A4 format up to 30000x10000 pixels and more. In a double For loop, each pixel is compared with a threshold. Below the threshold white, above black. I know that ReadPixel and WritePixel will not be faster. I need indicators. Unfortunately I do not have enough knowledge about this subject. I understand pointers as such, but I do not know how to apply them to a pixmap. I have read a little bit on various forums but I am still going in circles. Please help.


Midimaster

If the scanner file it is only 8bit you can work it without using any graphic commands. Just by reading the file as a BYTE-stream, manipulate the bytes and write the results to a second stream. This can be done extremely fast.
...back from North Pole.

Ryszard

I would have to save the tif uncompressed. Discard the header. Each image is thresholded about 20 times.

Midimaster

what do you mean with "is thresholded 20 times"? Do you mean that at the end you have 20 versions of the same image? Or do you mean that the same image is manipulated in 20 diffrent steps to get one final result?

what is your aim? to fasten the speed of the threshold job? To get better looking results? to automate the procedure?
what is the current state of your code? What are the deficiencies of the current code?
...back from North Pole.

Ryszard

The program that comes with the scanner can only threshold with one threshold. (It happens that threshold 0 is too high. This is probably a program error). For many drawings this is not enough. You need to set different thresholds for each area. Then you have to glue the drawing together to make it acceptable, readable. This takes less time than setting the "golden mean" in the scanning program, if you can.  I have been looking for information on local thresholding. I have tried using the ImageJ program. No success. I decided to use BlitzMax. I bought the first version for 80 Euro. I played with it not seriously. Some calculations. And now it's time to get serious. I scan with different thresholds, a little redundantly. Then I watch, select a few, remove unnecessary fragments and glue. Everything works as it should but you can do it faster so I would like to get that speed. Maybe I'll add options to remove some pixel junk.

Derron

#5
you could replace readpixel and writepixel with array like accesses to the pixmap data.

something like pixmap.pixels[y*pixmap.pitch+x] will return the int value of the pixmap at "x,y". Then do bitshifting to retrieve r,g,b,a... in a grayscale it might be enough to have "r".


If the x,y is not of much interest (so each pixel goes through the same "if gray > 128 then ...") you might even be able to use the array as "memory block" and iterate over it differently but I will leave that up for more experienced users to explain and providing examples for it.


bye
Ron

Midimaster

You are talking about different rectangular areas in the scan? Then an array is faster than a stream.

Reading, checking, and changing all values in an ByteArray with size [30000,10000] elements needs only 1.5sec.
Doing this with only 3000x1000 pixel is 100 times faster: 15msec

You could create a 3000x1000pix image from the scan and first work with this
Now you could divide this image into 100 (10x10) areas of 300x100 pix and each area gets an individual "threshold level point" in the middle of the area.
With the mouse you can increase or reduce each area level point's value.
Between the 100 level points you can interpolate intermediate values.
The new threshold values manipulate only the small image until the scan is optimized


At the end the 100 levelpoint and the interpolated values manipulate the original image and save it.


...back from North Pole.

Ryszard

I put the drawing into the scanner. I have a scan with a predetermined threshold from the previous drawing. The drawing is OK but the table is not visible at all. I set a higher threshold. The table appears. But not everything is visible yet. I set the threshold even higher. The table is OK. I look at the whole drawing. There is no drawing. One black spot. I lower the threshold to the limit of the table readability. It is a little better because I can see some drawing but with wide black spots. I leave it like this because the table is the most important.

Ryszard

He scans all the drawings in a given folder. A few, a dozen, more than a hundred. I process them with my program. It takes some time but it is acceptable. Using Irfanview (I have license) I load the first drawing, with lowest threshold. White image. Next. Something appears. Next, next. There's a drawing. Next. Next. The drawing's OK. Save it. The next drawing is already with black streaks. But the table with the description is still faintly visible. Next. The drawing is no longer visible, but the table is OK. I cut out unnecessary parts of the drawing and glue them both together. It is good. Sometimes I need to assemble more drawings but I almost always manage. If the drawing is OK, I just change the name. Sometimes in the table the archive number or the drawing number is very pale. If I enlarge the threshold to make the number clearly readable, the drawing is one big black spot.

Derron

So in essence you need an:
- image viewer
- image deleter ("throw away")
- image part selector ("keep this")
- image onion/layer merger (merge "base" + all "keep this" parts together)
- image export

The viewer:
Load your pixmap, scale the pixmap to a "reasonable size" (which your gpu could handle as texture), load as image and display it

The deleter / part selector / marker:
You could automate parts. If no "full scan" as basement was defined, use this until a "better basement" was selected. Have a "throw away" option and a rectangular marker (keep track of them - you might even need to be able to iterate over them / select them with the mouse to delete or "resize" ?).
Marked rectangles are rounded "upwards" when it comes to calculate the "real" dimensions (of the unscaled scan pixmap). Alternatively you could consider only showing parts of the image and keep it unscaled.

The onion slayer :)
Have your background pixmap and place all the marked elements on it. Optionally you might have all these parts "sortable" so you could define what overlays partially with other elements


Exporter:
Just export the pixmap via SavePixmapPNG or so.


you could even play with automatically making stuff "transparent" (or even _optional_ transparent so it only becomes transparent if overlaying other "parts", not the background image). You could simply make it transparent with a kind of "flood fill" from the outside - with a "tolerance" value etc.



You can use MaxGUI for this - or wxMax, this way it can become a regular "GUI" app with buttons, listboxes (for the rectangles) etc.


bye
Ron

fielder

#10
Quote from: Ryszard on March 12, 2021, 08:32:27
Using Irfanview (I have license) I load the first drawing, with lowest threshold. White image.
i think that you need to learn some basic image editing software.. (something like the very old paint shop pro) .. just change contrast/brightness on all the areas that need to be more "clear" ... seems a bit too long to create a image-editor from scratch (and maybe add to it some AI algorithms to speed up the work) when thare are a lot on the wild.

can i suggest GIMP2 ? (it's free)

Midimaster

Here my idea of a realtime threshold tester. This BlitzMax 1.50 code is a working example. Click on the left to set a new threshold, then fly around with the mouse in the test image.

copy the sample image I attached here to the same folder of the bmx-code.

test Threshold of 100, then 70 then 40 to see the difference. now you could think about 100 areas with 100 different threshold values and the image would be perfect on each area. What do you think?

Code (BlitzMax) Select
SuperStrict
Graphics 800,1000
Global Image:TImage=LoadImage("scan.png",DYNAMICIMAGE)
Global ImageB:TImage=LoadImage("scan.png",DYNAMICIMAGE)
Global Scan:TPixmap=LockImage(Image)
Global Backup:TPixmap=LockImage(ImageB)

Global Threshold%=128
Repeat
Cls
SetColor 255,255,255
For Local i%=0 To 1000 Step 40
DrawText i/4,30,i
Next 
DrawOval 70,Threshold*4,19,19


DrawPixmap scan ,100,100
Local zeit%=MilliSecs()
Scan=CopyPixmap(Backup)
If MouseX()<100
If MouseHit(1)
Threshold=MouseY()/4
EndIf
Else

Local Mx%=MouseX()-100
Local My%=MouseY()-100

For Local x%=0 To 599
For Local y%=0 To 799
If (x>MX-125) And (x<MX+125) And (y>MY-125) And (y<MY+125)

Local r:Long=ReadPixel(Scan,x,y)
If (r & 255)>Threshold
WritePixel Scan, x,y, $FFFFFFFF
Else
WritePixel Scan, x,y,0
EndIf
EndIf
Next
Next
EndIf
Print (MilliSecs()-Zeit)
Flip 0
Until AppTerminate()
...back from North Pole.

Derron

#12
If you compile this on Linux ... it will fail (scan.png <> Scan.png).  Windows is case-insensitive, Linux not, Mac only optional ...

I added mousewheel adjustment and a framework command to only compile what is needed (and changed it to "Scan.png" from "scan.png"). And you can close the app with the ESC key.


Code (Blitzmax) Select

SuperStrict
Framework  Brl.standardIO
Import Brl.GLMax2D
Import Brl.PNGLoader

Graphics 800,1000
Global Image:TImage=LoadImage("Scan.png",DYNAMICIMAGE)
Global ImageB:TImage=LoadImage("Scan.png",DYNAMICIMAGE)
Global Scan:TPixmap=LockImage(Image)
Global Backup:TPixmap=LockImage(ImageB)
Global MouseZBackup:Int

Global Threshold%=128
Repeat
Cls
SetColor 255,255,255
For Local i%=0 To 1000 Step 40
DrawText i/4,30,i
Next 
DrawOval 70,Threshold*4,19,19


DrawPixmap scan ,100,100
Local zeit%=MilliSecs()
Scan=CopyPixmap(Backup)

'adjust threshold
If MouseX()<100
If MouseHit(1)
Threshold=MouseY()/4
EndIf
EndIf
If MouseZ()-MouseZBackup <> 0
Threshold = Max(0, Min(255, Threshold + MouseZ() - MouseZBackup))
MouseZBackup = MouseZ()
EndIf

'show treshold adjustment preview
If MouseX()>=100
   
Local Mx%=MouseX()-100
Local My%=MouseY()-100

For Local x%=0 Until Scan.width
For Local y%=0 Until Scan.height
If (x>MX-125) And (x<MX+125) And (y>MY-125) And (y<MY+125)
   
Local r:Long=ReadPixel(Scan,x,y)
If (r & 255)>Threshold
WritePixel Scan, x,y, $FFFFFFFF
Else
WritePixel Scan, x,y,0
EndIf
EndIf
Next
Next
EndIf
Print (MilliSecs()-Zeit)
Flip 0
Until AppTerminate() Or KeyHit(KEY_ESCAPE)



Edit: If you need to work on the 10000*30000 pixmap you might get speed up when not calling ReadPixel() etc each time.

ReadPixel(x,y) uses PixelPtr(x,y) which is
Code (Blitzmax) Select

Method PixelPtr:Byte Ptr( x,y )
Return pixels+y*pitch+x*BytesPerPixel[format]
End Method


If you need to adjust EACH pixel regardless of their x,y then you could just access the pixels-pointer like an array (pixmap.pixels[0] would return the first byte of pixel "0,0").
With "BytesPerPixel[pixmap.format]" you could retrieve how many bytes each pixel uses.

So something like this can work too:
Code (BlitzMax) Select

Local bpp:Int = BytesPerPixel[Scan.format]
'pixmap.capacity would work for non static pixmaps too
Local byteCount:Int = Scan.width * Scan.height * bpp
For Local i:Int = 0 Until byteCount
If Scan.pixels[i] > Threshold
'this could be optimized I am sure
For Local b:Int = 0 Until bpp
Scan.pixels[i + b] = 255
Next
Else
'this could be optimized I am sure
For Local b:Int = 0 Until bpp
Scan.pixels[i + b] = 0
Next
EndIf

i :+ bpp - 1
Next


this will use the threshold adjustment on the whole pixmap (copy). On my computer the whole loop (copy + adjustment) takes 4ms. Using ReadPixel and WritePixel it requires 14-15ms.


Edit 2:
Code (Blitzmax) Select

For Local b:Int = 0 Until bpp
Scan.pixels[i + b] = 255
Next

If you knew that you always get grayscale RGB ... you can hardcode this of course.
Scan.Pixels[i+0] = 255
Scan.Pixels[i+1] = 255
Scan.Pixels[i+2] = 255

Might be even faster as no "for loop" has to be done.

Another option would be the conversion to "RGBA" as this is an "integer" - that way you could just store "opacque white"($FFFFFFFF) and "transparent black" (0) for each pixels color instead of accessing the bytes.


bye
Ron

Midimaster

Quote from: Derron on March 12, 2021, 12:06:28
...I added mousewheel adjustment and a framework command to only compile what is needed (and changed it to "Scan.png" from "scan.png"). And you can close the app with the ESC key....

My code is only an essential demonstration code written "on the fly", which shows how a possible way could be by using diffrerent areas. The idea is to do it it with areas, not with all!

Doing this with the complete 10000x30000pixels does not lead to the goal . Next step would be to define individual threshold for each area. I will show this with my next post.
...back from North Pole.

Derron

#14
I did not blame your code - I just extended it to be easier to "play with".

Also my code and suggestions have shown ways to skip "readpixel/writepixel". For now you do not know what the OP wants - he talked about 10000*30000 pixel scans. If they needed to be processed, then it will be better to have a fast approach to do so. Accessing the pixmap.pixels-memoryblock directly might be faster. This is what my posts wanted to show.
And to show that the buffer access is faster I just compared "full scan adjustment" speed (the window is too small ... and you would need to run it 1000 times to have better measureable differences as all are <1ms).


Also your "proof of concept" might possibly not work on all the ones trying it out (Scan vs scan). There are too many people ignoring case sensitivity (not saying you did - simply "rapid coding" mistake I assume). OP is a Windows user (IrfanView is Windows only for I think 2 decades now) so it wont be a problem for him - was just a side note as these are the minor bits in the code people often oversee.


I fully understand that you just wrote it "on the go" - so again, I do not blame you for mistakes or missing "bonus stuff". There is no need to go into "defense mode". We are here to help the OP, not to compete.


bye
Ron