Got it Jazz. Will keep in touch. Thanks.
No, rotating with liqufy does not work - the filter has flaws in face detection that do not guarantee correct frame orientation 50% of the time.
Are you using windows? You can install the latest version of Pytnon from the official website (check "add to PATH" when installing). This is a script that uses opencv to detect faces - this is not the most accurate method, but it worked great on the test folder. Save it to a text file, change the extension to .py, run in python:
import subprocess
import sys
import socket
import json
API_HOST = "127.0.0.1"
API_PORT_SEND = 6321
API_PORT_LISTEN = 6320
def check_module(module_name):
try:
__import__(module_name)
except ImportError:
print(f"Module {module_name} not found. Installing...")
return install_module(module_name)
else:
print(f"Module {module_name} alredy installed")
return True
def install_module(module_name):
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", module_name])
print(f"Module {module_name} succesfully installed!")
return True
except subprocess.CalledProcessError:
return False
check_module("opencv-python")
import cv2
def detect_faces_count(image_gray):
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)
faces = face_cascade.detectMultiScale(image_gray, scaleFactor=1.2, minNeighbors=7)
return len(faces)
def rotate_image(image, angle):
if angle == 0:
return image
elif angle == 90:
return cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
elif angle == 180:
return cv2.rotate(image, cv2.ROTATE_180)
elif angle == 270:
return cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
def find_correct_orientation_angle(image_path):
image = cv2.imread(image_path)
angles = [0, 90, 180, 270]
best_angle = 0
max_faces = -1
for angle in angles:
rotated = rotate_image(image, angle)
gray = cv2.cvtColor(rotated, cv2.COLOR_BGR2GRAY)
count = detect_faces_count(gray)
print(f"faces at {angle} degrees: {count}")
if count > max_faces:
max_faces = count
best_angle = angle
return best_angle
def send_data_to_jsx(message):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((API_HOST, API_PORT_SEND))
s.sendall(json.dumps(message).encode("utf-8"))
except Exception as e:
print(f"Error sending answer: {e}")
def start_local_server():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind((API_HOST, API_PORT_LISTEN))
srv.listen(1)
print("Server running")
while True:
client_socket, client_address = srv.accept()
print(f"Connection established with {client_address}")
try:
message = client_socket.recv(1024)
if message:
message = json.loads(message.decode("utf-8").rstrip("\n"))
print(f"Recieved message: {message}")
if message["type"] == "payload":
data = message["message"]
print(f"Processing payload {data}")
result = find_correct_orientation_angle(data)
print(f"Rotate {result}")
send_data_to_jsx({"type": "answer", "message": result})
elif message["type"] == "handshake":
send_data_to_jsx({"type": "answer", "message": "success"})
except Exception as e:
print(f"Error: {e}")
send_data_to_jsx({"type": "answer", "message": None})
sys.exit()
finally:
client_socket.close()
start_local_server()
When you first run it, it will download and install the opencv library, and will be ready to work when the "Server running" message appears. In case of poor face recognition, you can read on the Internet about setting up the detectMultiScale scaleFactor and minNeighbors parameters
Save the second script for Photoshop in .jsx format, it saves the current file to a temporary folder, sends a request to the running python script and rotates by the angle received in the response:
#target photoshop
const API_HOST = '127.0.0.1',
API_PORT_SEND = 6320,
API_PORT_LISTEN = 6321,
API_DELAY = 10000;
var OpenCV = new SDApi(API_HOST, API_PORT_SEND, API_PORT_LISTEN),
apl = new AM('application'),
doc = new AM('document');
try { init() } catch (e) {
alert(e)
}
function init() {
if (apl.getProperty('numberOfDocuments')) {
if (OpenCV.initialize()) {
var f = new File(Folder.temp + '/OpenCV.jpg');
doc.saveACopy(f);
var result = OpenCV.sendPayload(f.fsName.replace(/\\/g, '\\\\'));
f.remove();
if (result) doc.rotateDocument(result)
}
}
}
function SDApi(apiHost, portSend, portListen) {
this.initialize = function () {
var result = sendMessage({ type: 'handshake', message: {} }, true, API_DELAY)
if (!result) throw new Error('cannot connect to python!')
return true
}
this.exit = function () {
sendMessage({ type: 'exit' })
}
this.sendPayload = function (payload) {
var result = sendMessage({ type: 'payload', message: payload }, true, API_DELAY)
if (result) return result['message']
return null;
}
function sendMessage(o, getAnswer, delay) {
var tcp = new Socket;
tcp.open(apiHost + ':' + portSend, 'UTF-8')
tcp.writeln(objectToJSON(o))
tcp.close()
if (getAnswer) {
var t1 = (new Date).getTime(),
t2 = 0;
var tcp = new Socket;
if (tcp.listen(portListen, 'UTF-8')) {
for (; ;) {
t2 = (new Date).getTime()
if (t2 - t1 > delay) return null;
var answer = tcp.poll();
if (answer != null) {
var a = eval('(' + answer.readln() + ')');
answer.close();
return a;
}
}
}
}
}
function objectToJSON(obj) {
if (obj === null) {
return 'null';
}
if (typeof obj !== 'object') {
return '"' + obj + '"';
}
if (obj instanceof Array) {
var arr = [];
for (var i = 0; i < obj.length; i++) {
arr.push(objectToJSON(obj[i]));
}
return '[' + arr.join(',') + ']';
}
var keys = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
var result = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = objectToJSON(obj[key]);
result.push('"' + key + '":' + value);
}
return '{' + result.join(',') + '}';
}
}
function AM(target, order) {
var s2t = stringIDToTypeID,
t2s = typeIDToStringID,
AR = ActionReference,
AD = ActionDescriptor;
target = target ? s2t(target) : null;
this.getProperty = function (property, descMode, id, idxMode) {
property = s2t(property);
(r = new AR).putProperty(s2t('property'), property);
id != undefined ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id)) :
r.putEnumerated(target, s2t('ordinal'), order ? s2t(order) : s2t('targetEnum'));
return descMode ? executeActionGet(r) : getDescValue(executeActionGet(r), property);
}
this.hasProperty = function (property, id, idxMode) {
property = s2t(property);
(r = new AR).putProperty(s2t('property'), property);
id ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id))
: r.putEnumerated(target, s2t('ordinal'), s2t('targetEnum'));
try { return executeActionGet(r).hasKey(property) } catch (e) { return false }
}
this.saveACopy = function (pth) {
(d1 = new AD).putInteger(s2t('extendedQuality'), 12);
d1.putEnumerated(s2t('matteColor'), s2t('matteColor'), s2t('none'));
(d = new AD).putObject(s2t('as'), s2t('JPEG'), d1);
d.putPath(s2t('in'), pth);
d.putBoolean(s2t('copy'), true);
executeAction(s2t('save'), d, DialogModes.NO);
}
this.rotateDocument = function (angle) {
(r = new ActionReference()).putEnumerated(s2t("document"), s2t("ordinal"), s2t("targetEnum"));
(d = new ActionDescriptor()).putReference(s2t("null"), r);
d.putUnitDouble(s2t("angle"), s2t("angleUnit"), angle);
executeAction(s2t("rotateEventEnum"), d, DialogModes.NO);
}
function getDescValue(d, p) {
switch (d.getType(p)) {
case DescValueType.OBJECTTYPE: return { type: t2s(d.getObjectType(p)), value: d.getObjectValue(p) };
case DescValueType.LISTTYPE: return d.getList(p);
case DescValueType.REFERENCETYPE: return d.getReference(p);
case DescValueType.BOOLEANTYPE: return d.getBoolean(p);
case DescValueType.STRINGTYPE: return d.getString(p);
case DescValueType.INTEGERTYPE: return d.getInteger(p);
case DescValueType.LARGEINTEGERTYPE: return d.getLargeInteger(p);
case DescValueType.DOUBLETYPE: return d.getDouble(p);
case DescValueType.ALIASTYPE: return d.getPath(p);
case DescValueType.CLASSTYPE: return d.getClass(p);
case DescValueType.UNITDOUBLE: return (d.getUnitDoubleValue(p));
case DescValueType.ENUMERATEDTYPE: return { type: t2s(d.getEnumerationType(p)), value: t2s(d.getEnumerationValue(p)) };
default: break;
};
}
}
It's not very elegant, but it seems to work:
Perhaps this will help you.