Copy link to clipboard
Copied
In a bitmap, is it possible to identify single pixels sticking out from a shape? Let's say I have this black shape in a black-and-white bitmap:
xxxxxxxx
xxxxxxxx
xxxxxxxx
xxxxxxxx
x
an 8 by 4 rectangle with one pixel sticking out from the bottom. Question is, can I find such pixels using a script (JS) and make it white?
Thanks,
Peter
Copy link to clipboard
Copied
Peter, I don't know a method for working with a bitmap image. (there is NO histogram in bitmap mode) Would you be able to switch modes to grayscale and back? If so then this may work although Im NOT sure if I've overlooked something here. I only tested with a small file. I would not expect this kind of thing to be fast.
$.sleep(500); & WaitForRedraw(); can come out there just so I can see whats happening in the GUI.
#target photoshop
app.bringToFront();
function main() {
if (app.documents.length == 0) {
alert('You have NO document open?');
return;
}
var whiteRef = new SolidColor();
whiteRef.rgb.red = 255;
whiteRef.rgb.green = 255;
whiteRef.rgb.blue = 255;
var docRef = app.activeDocument;
with (docRef) {
// Horizontal selection
selection.select([[0, 0], [width , 0], [width, 1], [0, 1]]);
if (channels[0].histogram[0] == 1) {
alert('Single Pixel');
selection.fill(whiteRef);
}
for (var i = 0; i < height-1; i++) {
selection.translateBoundary(0, 1);
$.sleep(500);
WaitForRedraw();
if (channels[0].histogram[0] == 1) {
alert('Single Pixel');
selection.fill(whiteRef);
}
}
// Vertical selection
selection.select([[0, 0], [1 , 0], [1, height], [0, height]]);
if (channels[0].histogram[0] == 1) {
alert('Single Pixel');
selection.fill(whiteRef);
}
for (var i = 0; i < width-1; i++) {
selection.translateBoundary(1, 0);
$.sleep(500);
WaitForRedraw();
if (channels[0].histogram[0] == 1) {
alert('Single Pixel');
selection.fill(whiteRef);
}
}
selection.deselect();
}
};
main();
// Redraw the Screen
function WaitForRedraw() {
var eventWait = charIDToTypeID('Wait');
var enumRedrawComplete = charIDToTypeID('RdCm');
var typeState = charIDToTypeID('Stte');
var keyState = charIDToTypeID('Stte');
var desc = new ActionDescriptor();
desc.putEnumerated(keyState, typeState, enumRedrawComplete);
executeAction(eventWait, desc, DialogModes.NO);
}
Copy link to clipboard
Copied
Muppet,
Thanks for this. Temporarily converting to greyscale is not a problem. But unfortunately, your script breaks on this line:
selection.translateBoundary (0, 1);
with the error message "The document does not contain a selection". When I step through the script, I can see that it selects the first row of pixels, takes the first iteration in the for-loop fine, then in the second iteration the script breaks with the above-mentioned error message. So it looks as if the selection gets undone.
Scanning for lines with one pixel which are otherwise empty is not the right approach, though. I realise that my example may have led you to believe that it was, so let me give you a real example (see the screenshot). The line that the single pixel sits on has some other pixels set as well. So ideally, I would want to be able to look for a pixel that has one side adjacent to its own colour, the others to the opposite colour. In other words, if you're a black pixel and are surrounded by three white pixels and one black pixel, then you're single and you should change colour.
Sorry I didn't state this better earlier.
Thanks,
Peter
Copy link to clipboard
Copied
Just a theory..
If it is a rectangle shape you might be able to :-
Select the area
Get the selection bounds
Make a work path set to 1 (One of the bounds should be one less than the selection bounds) also check that there is only 4 pathpoints.
Then compare each bound, then you could make a new selection and fill.
Copy link to clipboard
Copied
Paul,
Thanks for the suggestion. But I'd prefer not to make a selection -- I've got hundreds of these things...
Peter
Copy link to clipboard
Copied
Ah now I see, another idea.
Select all the black ..
function selectBlack() {
var desc63 = new ActionDescriptor();
desc63.putInteger( charIDToTypeID('Fzns'), 13 );
var desc64 = new ActionDescriptor();
desc64.putDouble( charIDToTypeID('Lmnc'), 0.000000 );
desc64.putDouble( charIDToTypeID('A '), 0.000000 );
desc64.putDouble( charIDToTypeID('B '), 0.000000 );
desc63.putObject( charIDToTypeID('Mnm '), charIDToTypeID('LbCl'), desc64 );
var desc65 = new ActionDescriptor();
desc65.putDouble( charIDToTypeID('Lmnc'), 0.000000 );
desc65.putDouble( charIDToTypeID('A '), 0.000000 );
desc65.putDouble( charIDToTypeID('B '), 0.000000 );
desc63.putObject( charIDToTypeID('Mxm '), charIDToTypeID('LbCl'), desc65 );
desc63.putInteger( stringIDToTypeID('colorModel'), 0 );
executeAction( charIDToTypeID('ClrR'), desc63, DialogModes.NO );
};
Make a work path tolerance of 1, make selection from work path, invert and fill with white.
Copy link to clipboard
Copied
Paul,
Do you get a stairstep( corner points only ) path that way. When I try the path has smooth points and doesn't follow the shape. Do you know if there is a preference?
Copy link to clipboard
Copied
Yes I'm getting a staircase, but not selecting single pixels. Using workpaths can produce some strange results, I did a test with a rectangle with one pixel sticking out on one side at a time, this code worked for the Left, Right and Bottom but the Top was totally screwed up?
#target photoshop
main();
function main(){
DEBUG=true;
selectBlack();
var savedState = activeDocument.activeHistoryState;
var startRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
var savedState = activeDocument.activeHistoryState;
try{
var SB = activeDocument.selection.bounds;
}catch(e){alert(e+" \r"+e.line);
activeDocument.activeHistoryState = savedState;
return;
}
activeDocument.selection.makeWorkPath(1.0);
var workPath = activeDocument.pathItems.getByName("Work Path");
return;
if(workPath.subPathItems[0].pathPoints.length != 4){
activeDocument.activeHistoryState = savedState;
return;
}
var PB=[];
if(DEBUG){
$.writeln( workPath.subPathItems[0].pathPoints[0].anchor);
$.writeln( workPath.subPathItems[0].pathPoints[1].anchor);
$.writeln( workPath.subPathItems[0].pathPoints[2].anchor);
$.writeln( workPath.subPathItems[0].pathPoints[3].anchor);
}
PB[0] = workPath.subPathItems[0].pathPoints[0].anchor[0]; //Left
PB[1] = workPath.subPathItems[0].pathPoints[0].anchor[1]; //Top
PB[2] = workPath.subPathItems[0].pathPoints[1].anchor[0]; //Right
PB[3] = workPath.subPathItems[0].pathPoints[2].anchor[1]; //Bottom
var side = 9;
if(Number(SB[0]) != Number(PB[0]) ) side = 0;
if(Number(SB[1]) != Number(PB[1]) ) side = 1;
if(Number(SB[2]) != Number(PB[2]) ) side = 2;
if(Number(SB[3]) != Number(PB[3]) ) side = 3;
activeDocument.activeHistoryState = savedState;
activeDocument.selection.deselect();
if(DEBUG){
$.writeln(SB.toString());
$.writeln(PB.toString());
}
switch(side){
case 0: selSide(PB[0]-1,PB[1],PB[0],PB[3]); break;
case 1: selSide(PB[0],PB[1]-1,PB[2],PB[1]); break;
case 2: selSide(PB[2],PB[1],PB[2]+1,PB[3]); break;
case 3: selSide(PB[0],PB[3],PB[2],PB[3]+1); break;
default : break;
}
app.preferences.rulerUnits = startRulerUnits;
}
function selSide(Left,Top,Right,Bottom){
activeDocument.selection.select([[Left,Top],[Right,Top],[Right,Bottom],[Left,Bottom]], SelectionType.REPLACE, 0, false);
var White = new SolidColor;
White.rgb.hexValue = 'ffffff';
activeDocument.selection.fill(White);
activeDocument.selection.deselect();
}
function selectBlack() {
var desc63 = new ActionDescriptor();
desc63.putInteger( charIDToTypeID('Fzns'), 13 );
var desc64 = new ActionDescriptor();
desc64.putDouble( charIDToTypeID('Lmnc'), 0.000000 );
desc64.putDouble( charIDToTypeID('A '), 0.000000 );
desc64.putDouble( charIDToTypeID('B '), 0.000000 );
desc63.putObject( charIDToTypeID('Mnm '), charIDToTypeID('LbCl'), desc64 );
var desc65 = new ActionDescriptor();
desc65.putDouble( charIDToTypeID('Lmnc'), 0.000000 );
desc65.putDouble( charIDToTypeID('A '), 0.000000 );
desc65.putDouble( charIDToTypeID('B '), 0.000000 );
desc63.putObject( charIDToTypeID('Mxm '), charIDToTypeID('LbCl'), desc65 );
desc63.putInteger( stringIDToTypeID('colorModel'), 0 );
executeAction( charIDToTypeID('ClrR'), desc63, DialogModes.NO );
};
Copy link to clipboard
Copied
Paul,
Thanks for the great effort. I get the same results as you, unfortunately. It excludes the single pixels, but some other things as well. But anyway, it's been educational. I'll dig into the path and pathPoints and see if there's anything there. In the meantime, if you find anything I'd be grateful if you'd let me know.
Thanks again,
Peter
Copy link to clipboard
Copied
artLayers[0].applyMaximum(1);
artLayers[0].applyMinimum(1);
May proved you a get out of jail card if odd pixels are stranded along edges… At least this one would be fast…
Copy link to clipboard
Copied
When I make a path from a selection I don't get stairstep paths. And I can't find a way to make Photoshop create one. I deleted my preferences - no joy.
Anyone have any ideas?
attached is an example path made from a selection. The image was similar to the one posted but sized so that each stray block was 1 px
Copy link to clipboard
Copied
Mike, I get the same as you no matter what.
Copy link to clipboard
Copied
That is good to know. I was beginning to think there was something wrong with my install of Photoshop.
I wonder why it creates a stairstep path for some.
Copy link to clipboard
Copied
Mike, I have changed all sorts of settings whilst trying to do this and never been able to work it out. If you don't mind using another CS app then you should be able to get pixel perfect vectors (well as close as I can work out and its reliable) that you could paste back to Photoshop. Illustrator has a great tracing function that is fully scriptable. Here is an example for a black & white trace… In my tests I used a bitmap converted to grayscale to place n trace… Takes a few seconds but very neat…
#target illustrator
var docRef = app.activeDocument;
with (docRef) {
var thisImage = placedItems[0].trace();
redraw();
// Get tracing options object
var thisTrace = thisImage.tracing.tracingOptions;
// Adjust the properties
with (thisTrace) {
cornerAngle = 90;
fills = true;
livePaintOutput = false;
minArea = 1;
pathFitting = 0.0;
preprocessBlur = 0.0;
resample = false;
strokes = false;
threshold = 128;
tracingMode = TracingModeType.TRACINGMODEBLACKANDWHITE;
viewRaster = ViewRasterType.TRACINGVIEWRASTERTRANSPARENTIMAGE;
viewVector = ViewVectorType.TRACINGVIEWVECTOROUTLINESWITHTRACING;
}
var thisGroup = thisImage.tracing.expandTracing();
redraw();
with (thisGroup) {
// Remove the white path items
for (var i = pathItems.length-1; i >= 0; i--) {
if (pathItems.fillColor.gray == 0) {
pathItems.remove();
}
}
// Remove the white compound path items
for (var i = compoundPathItems.length-1; i >= 0; i--) {
if (compoundPathItems.pathItems[0].fillColor.gray == 0) {
compoundPathItems.remove();
}
}
// Ungroup the paths
for (var i = pageItems.length-1; i >= 0; i--) {
pageItems.move(thisGroup, ElementPlacement.PLACEBEFORE);
}
}
// Make all the points angled corners
for (var i = 0; i < pathItems.length; i++) {
for (var j = 0; j < pathItems.pathPoints.length; j++) {
pathItems.pathPoints
pathItems.pathPoints
pathItems.pathPoints
}
}
redraw();
}
I've still got some work to do on this yet as I would like to remove all the unnecessary path points. I think Im going to need some trigonometry to work out if the path changes direction but I haven't used trig since school n that was some time ago…
Might also be of use to Peter depending on what his bitmaps were being used for could use illustrator .ai files instead.
Copy link to clipboard
Copied
Thanks Mark. I was more troubled by my paths being wrong than the script not working. But it's good to know how to get a path that exactly matches the selection when I need one.
BTW, I don't think you need trig to determine if the path changes direction if all the points are corner points. You just need to compare three points at a point. The prev, current, and next. If the x is the same in all three it's a horz line. If the y is the same, it's vert. But working out the logic of the condition check may be as hard as forgotten trig. This is not tested.
var isHorz = ( Math.max(pathPoint[c-1].anchor[0],pathPoint
var isVert = ( Math.max(pathPoint[c-1].anchor[1],pathPoint
if( !isHorz || !isVert ) alert('direction change');
Copy link to clipboard
Copied
Mike, Thanks I will look at that. I went with the trig as this will be part of some other things I may need for AI. I got close but NO cigar. I could not get my head around a clean way to check last-zero-first pathpoints as part of the the loop!!! So one or two points remain that are NOT really needed. Here is how I was trying…
#target illustrator
var docRef = app.activeDocument;
with (docRef) {
var thisImage = placedItems[0].trace();
redraw();
// Get tracing options object
var thisTrace = thisImage.tracing.tracingOptions;
// Adjust the properties
with (thisTrace) {
cornerAngle = 90;
fills = true;
livePaintOutput = false;
minArea = 1;
pathFitting = 0.0;
preprocessBlur = 0.0;
resample = false;
strokes = false;
threshold = 128;
tracingMode = TracingModeType.TRACINGMODEBLACKANDWHITE;
viewRaster = ViewRasterType.TRACINGVIEWRASTERTRANSPARENTIMAGE;
viewVector = ViewVectorType.TRACINGVIEWVECTOROUTLINESWITHTRACING;
}
var thisGroup = thisImage.tracing.expandTracing();
redraw();
with (thisGroup) {
// Remove the white path items
for (var i = pathItems.length-1; i >= 0; i--) {
if (pathItems.fillColor.gray == 0) {
pathItems.remove();
}
}
// Remove the white compound path items
for (var i = compoundPathItems.length-1; i >= 0; i--) {
if (compoundPathItems.pathItems[0].fillColor.gray == 0) {
compoundPathItems.remove();
}
}
// Ungroup the paths
for (var i = pageItems.length-1; i >= 0; i--) {
pageItems.move(thisGroup, ElementPlacement.PLACEBEFORE);
}
}
// Make all the points angled corners
for (var i = 0; i < pathItems.length; i++) {
for (var j = 0; j < pathItems.pathPoints.length; j++) {
pathItems.pathPoints
pathItems.pathPoints
pathItems.pathPoints
}
}
// Remove unwanted points (NOT quite right!!! but close)
for (var i = 0; i < pathItems.length; i++) {
for (var j = pathItems.pathPoints.length-2; j >= 1; j--) {
var pointAfter = pathItems.pathPoints[j + 1].anchor;
var pointBefore = pathItems.pathPoints[j - 1].anchor;
var myAngle = inverseArcTan(pointAfter, pointBefore);
if (myAngle == 0 || myAngle == 90 || myAngle == -90) {
pathItems.pathPoints
}
}
}
redraw();
}
function inverseArcTan(xy1, xy2) {
// Opposite & Adjacent Sides
var oppSide = (xy2[1] - xy1[1]);
var adjSide = (xy2[0] - xy1[0]);
// Radians to Degrees
var myAngle = (Math.atan(oppSide / adjSide)) * (180 / Math.PI);
return myAngle;
}
Copy link to clipboard
Copied
For me, it's too much math and too much to keep track of to do something like this in a script.
For the first point test, couldn't you use the lenght offset as the pointBefore and for the last use the start offset for pointAfter.
But I question when you delete the point doesn't that change the pathPoints length and thereby effect pointAfter and pointBefore? Also doesn't deleting a point chage angle of the prev and next point?
Copy link to clipboard
Copied
Mark,
> Might also be of use to Peter depending on what his bitmaps were being used for could use illustrator .ai files instead.
ai files wouldn't be a problem. This is for a book I'm working on about a man who contributed a lot to the development of stenography in the 17th century). Each symbol he devised was scanned for the authors, but not particularly well and at too low a resolution. Rescanning is not an option, apparently, and I've been polishing up the files manually. I've scripted some things (trimming, bitmapping), and the single-pixel removal would be nice too. As there are 550 of these, I want to batch-process as much as possible.
As I said, this thread has been very educational for me so far -- thanks for that (and to Michael).
Peter
Copy link to clipboard
Copied
Have you tried Marks method yet?
activeDocument.activeLayer.applyMaximum(4);
activeDocument.activeLayer.applyMinimum(4);
It seems to work fine, just experiment with the values.
Copy link to clipboard
Copied
Paul this worked for me as a batch process on the mac. Inverse filter again inverse back should clean out what would have been stray white pixels.
#target photoshop
app.bringToFront();
while (app.documents.length) {
app.activeDocument.close(SaveOptions.PROMPTTOSAVECHANGES);
}
var defaultFolder = new Folder ('~/Desktop');
var inputFolder = defaultFolder.selectDlg('Please select your Folder of Bitmap files…');
var outputFolder = defaultFolder.selectDlg('Please Make/Select a Folder to save Cleaned up files to…');
if (inputFolder != null && outputFolder != null) {
var fileList = inputFolder.getFiles(fileFiltering);
if (fileList.length > 0) {
main(fileList);
} else {
alert('This Folder contained NO Photoshop Tiff files!');
}
} else {
alert('A Required folder was NOT chosen!!!');
}
// Main Photoshop file processing
function main(fileObjs) {
with (app) {
displayDialogs = DialogModes.NO;
for (var i = 0; i < fileObjs.length; i++) {
if (fileList instanceof File) {
open(fileObjs);
var docRef = activeDocument;
with (docRef) {
var baseName = app.activeDocument.name.slice(0,-4);
if (mode != DocumentMode.GRAYSCALE) changeMode(ChangeMode.GRAYSCALE);
docRes = resolution;
$.write(docRes + '\n');
// Cleans single pixels
artLayers[0].applyMaximum(1); // Adjust as required
artLayers[0].applyMinimum(1); // Ditto
artLayers[0].invert();
artLayers[0].applyMaximum(1); // Ditto
artLayers[0].applyMinimum(1); // Ditto
artLayers[0].invert();
var thisBitmap = bitmapOptions(docRes)
changeMode(ChangeMode.BITMAP, thisBitmap);
var newFilePath = new File(outputFolder + '/' + baseName + '.tif');
SaveFileasTIFF(newFilePath, false, TIFFEncoding.TIFFLZW, false, false, false);
close(SaveOptions.DONOTSAVECHANGES);
}
}
}
}
}
function bitmapOptions(res) {
bitOptions = new BitmapConversionOptions();
//bitOptions.angle = 0;
//bitOptions.frequency = 150;
bitOptions.method = BitmapConversionType.HALFTHRESHOLD;
//bitOptions.pattenName = '';
bitOptions.resolution = res;
bitOptions.shape = BitmapHalfToneType.SQUARE;
return bitOptions;
}
function SaveFileasTIFF(saveFile, aC, iC, la, sC, tr) {
tiffSaveOptions = new TiffSaveOptions();
tiffSaveOptions.alphaChannels = aC;
tiffSaveOptions.byteOrder = ByteOrder.MACOS;
tiffSaveOptions.embedColorProfile = true;
tiffSaveOptions.imageCompression = iC;
tiffSaveOptions.layers = la;
tiffSaveOptions.spotColors = sC;
tiffSaveOptions.transparency = tr;
activeDocument.saveAs(saveFile, tiffSaveOptions, true, Extension.LOWERCASE);
}
// Mac ONLY filtering (Photoshop Tiff's)
function fileFiltering(fileObj) {
if (fileObj.creator == '8BIM' && fileObj.type == 'TIFF') {
return true;
} else {
return false;
}
}
Copy link to clipboard
Copied
Oh, I had a question to ask too… Im CS2 and for reasons I can't work out I can't…
$.writeIn('Foo'); // Returns '$.writeIn is not a function'
But I can…
$.write('Foo' + '\n');
Am I just being thick here!!!?
Copy link to clipboard
Copied
Nice Mark, I have just copied your $.writeIn('Foo'); and pasted into ESTK and it pastes in as a capitol "I"
it should be all lowercase $.writeln('Foo');
Copy link to clipboard
Copied
Time to go to specsavers I think… Im spending too much time looking at my screen… TVM Paul.
Copy link to clipboard
Copied
Mark,
That's absolutely fabulous! This works very well. I noticed though that when single pixels are at the edge, the whole line is made black. Suppose you have something like the top picture in the screenshot. Your script changes that to what you see in the second part of the screenshot. I don't know how difficult it is to cope with that in your script, but a workaround I found is to add a two-pixel border, run the script, then remove the border. That does the trick.
I also noticed that if you comment out the second pair of applyMin/Max, so that you get this:
artLayers[0].applyMaximum (1); // Adjust as required
artLayers[0].applyMinimum (1); // Ditto
artLayers[0].invert ();
artLayers[0].invert ();
Any white single-pixel "inlets" are left alone. This makes for a very flexible script.
Thanks very much for this script!
Regards,
Peter
Copy link to clipboard
Copied
Peter, before I recalled this old filter method. I tried another pixel selection where I would use test histogram expand retest histogram contract.
It was painfully slow so I did not bother to post. But to get that to work I also had to increase canvas +4 pixels to get the edges right then resize back. Happy its been of use…