poto-siril
Version:
Automatization around Siril (<https://siril.org/>) for deep sky astrophotography.
304 lines (268 loc) • 9.19 kB
text/typescript
import path from "path";
import fs from "fs-extra";
import { ImageSpec, FileImageSpec } from "./types";
import { logger } from "./logger";
/**
* Retrieve FITS files from a source directory.
*/
export const getFitsFromDirectory = ({
directory: directory,
projectDirectory,
}: {
directory: string;
projectDirectory: string;
}) => {
const files: fs.Dirent[] = fs.readdirSync(directory, {
recursive: true,
withFileTypes: true,
encoding: "utf8",
});
const fileImageSpecs: FileImageSpec[] = [];
let previousFile: FileImageSpec | null = null;
// Process the list of files.
files.forEach(file => {
if (file.isDirectory()) {
return;
}
if (file.name.endsWith("_thn.jpg")) {
logger.debug(`Skipping thumbnail file (${file.name})`);
return;
}
if (!(file.isFile() || file.isSymbolicLink())) {
logger.debug(`Skipping not-file or symlink (${file.name})`);
return;
}
// Ignore macOS files.
if (
file.name === ".DS_Store" ||
file.name.startsWith("._") ||
file.parentPath.includes("._")
) {
return;
}
// Ignore Windows files.
if (file.name === "Thumbs.db") {
return;
}
if (!file.name.endsWith(".fit")) {
logger.debug(`Skipping unknown file (${file.name})`);
return;
}
// If the file is a FITS file, process it.
const specs = getFileImageSpecFromFilename(
file,
projectDirectory,
previousFile,
);
previousFile = specs;
fileImageSpecs.push(specs);
});
return fileImageSpecs;
};
/**
* Retrieve Light / Flat, Bulb duratin, binning, filter, gain, date, time, temperature, frame number in the sequence, sequence unique identifier.
* @param filename
*/
export const getFileImageSpecFromFilename = (
fileFS: fs.Dirent,
projectDirectory: string,
previousFile: FileImageSpec | null,
): FileImageSpec => {
// Light_LDN 1093_120.0s_Bin1_H_gain100_20240707-002348_-10.0C_0001.fit
// Flat_810.0ms_Bin1_H_gain0_20240707-102251_-9.9C_0019.fit
// Dark_300.0s_Bin1_L_gain360_20230910-101917_-9.8C_0001.fit
// Bias_1.0ms_Bin1_L_gain100_20240308-154938_-9.9C_0003.fit
// Note: Filter is not always specified in the filename.
const regex =
/^(?<type>[A-Za-z]+)(?:_[^_]+)?_(?<bulb>[^_]+)_(?<bin>Bin\d)_((?<filter>[A-Za-z0-9 ]+)_)?gain(?<gain>\d+)_(?<datetime>\d{8}-\d{6})_(?<temperature>-?\d+\.\d+C)_(?<sequence>\d{4})\.(?<extension>fit)$/;
const match = fileFS.name.match(regex);
if (match && match.groups) {
const sequencePosition = parseInt(match.groups.sequence, 10);
const datetime = parseDate(match.groups.datetime);
const temperature = parseFloat(match.groups.temperature);
const file = {
setName: "",
type: match.groups.type,
bulb: match.groups.bulb,
bulbMs: parseBulbString(match.groups.bulb),
bin: match.groups.bin,
filter: match.groups.filter?.replaceAll(" ", "").trim() ?? null,
gain: parseInt(match.groups.gain, 10),
sequenceId: "", // To be determined later. Referenced here early to have serialization printing fields in this order.
sequencePosition,
datetime,
temperature,
fileName: fileFS.name,
extension: match.groups.extension,
sourceFileDirectory: fileFS.parentPath,
sourceFilePath: path.join(fileFS.parentPath, fileFS.name),
} as FileImageSpec;
file.setName = getSetName(file);
if (!previousFile) {
file.sequenceId = unParseDate(datetime);
} else {
// Is considered part of the same sequence if the previous file is of the same type and the sequence position is bigger (not only by 1 to allow sequences with holes in it.).
file.sequenceId =
previousFile.setName === file.setName &&
previousFile.sequencePosition < sequencePosition
? previousFile.sequenceId
: unParseDate(datetime);
}
if (file.type === "Light") {
file.projectFileDirectory = undefined; // To be determined later from layerSetId. Referenced here early to have serialization printing fields in this order.
} else if (file.type === "Flat") {
const directory = file.setName;
file.projectFileDirectory = file.filter
? path.join(projectDirectory, file.filter, directory)
: path.join(projectDirectory, directory);
} else {
const directory = `${file.type}_${file.bulb}_${file.bin}_gain${file.gain}`;
file.projectFileDirectory = path.join(projectDirectory, "any", directory); // We ignore the filter for darks and biases.
}
if (file.type === "Light") {
file.projectFilePath = undefined; // To be determined later from layerSetId. Referenced here early to have serialization printing fields in this order.
} else {
file.projectFilePath = path.join(
file.projectFileDirectory,
file.fileName,
);
}
return file;
} else {
throw new Error(
`Filename ${fileFS.name} does not match the expected pattern for Specs extraction.`,
);
}
};
/**
* @param datetimeString `20240707-002348` format.
*/
const parseDate = (datetimeString: string): Date => {
const datetimeRegex = /(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/;
const matchResult = datetimeString.match(datetimeRegex);
if (matchResult) {
const [_, year, month, day, hour, minute, second] = matchResult;
const parsedDate = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1, // Month is zero-based in JavaScript Date.
parseInt(day, 10),
parseInt(hour, 10),
parseInt(minute, 10),
parseInt(second, 10),
);
return parsedDate;
} else {
throw new Error(`Invalid datetime string: ${datetimeString}`);
}
};
/**
* Inverse function of `parseDate`.
* @returns `20240707-002348` format.
*/
const unParseDate = (datetime: Date): string => {
const pad = (num: number) => num.toString().padStart(2, "0");
return `${datetime.getFullYear()}${pad(datetime.getMonth() + 1)}${pad(
datetime.getDate(),
)}-${pad(datetime.getHours())}${pad(datetime.getMinutes())}${pad(
datetime.getSeconds(),
)}`;
};
/**
*
* @param bulbString `120.0s` or `810.0ms`.
* @returns 120000 or 810 (in ms).
*/
const parseBulbString = (bulbString: string): number => {
const bulbRegex = /(\d+\.\d+)(ms|s)/;
const matchResult = bulbString.match(bulbRegex);
if (matchResult) {
const [_, bulb, unit] = matchResult;
return unit === "s" ? parseFloat(bulb) * 1000 : parseFloat(bulb);
} else {
throw new Error(`Invalid bulb string: ${bulbString}`);
}
};
export const copyFileToProject = (
file: FileImageSpec,
alreadyImported: FileImageSpec[],
): FileImageSpec[] => {
if (!fs.existsSync(file.projectFileDirectory)) {
fs.mkdirSync(file.projectFileDirectory, { recursive: true });
}
if (alreadyImported.find(f => f.fileName === file.fileName)) {
logger.debug(`- ${file.fileName} already imported.`);
} else {
fs.copyFileSync(file.sourceFilePath, file.projectFilePath);
alreadyImported.push(file);
logger.debug(`- ${file.fileName} imported.`);
}
return alreadyImported;
};
/**
* Used to match flats with biases, lights with darks.
* Allow these couples:
* - [A] with [B]
* - Light with Dark.
* - Light with Flat (allowing for different gain).
* - Flat with Bias.
*/
export const matchSetFile = (A: ImageSpec, B: ImageSpec): boolean => {
if (A.type === "Light" && B.type === "Dark") {
return A.bulb === B.bulb && A.bin === B.bin && A.gain === B.gain;
} else if (A.type === "Light" && B.type === "Flat") {
return A.bin === B.bin && A.filter === B.filter;
} else if (A.type === "Flat" && B.type === "Bias") {
return A.bin === B.bin && A.gain === B.gain;
} else {
return false;
}
};
/**
* @returns `Flat_520.0ms_Bin1_H_gain0`, `Flat_520.0ms_Bin1_gain0` format.
*/
const getSetName = (file: FileImageSpec): string => {
return file.filter
? `${file.type}_${file.bulb}_${file.bin}_${file.filter}_gain${file.gain}`
: `${file.type}_${file.bulb}_${file.bin}_gain${file.gain}`;
};
/**
* Utils for map.
*/
export const getImageSpecFromSetName = (setName: string): ImageSpec => {
return setName.split("_").length === 5
? ({
setName,
type: setName.split("_")[0],
bulb: setName.split("_")[1],
bin: setName.split("_")[2],
filter: setName.split("_")[3],
gain: Number(setName.split("_")[4].replace("gain", "")),
} as ImageSpec)
: ({
setName,
type: setName.split("_")[0],
bulb: setName.split("_")[1],
bin: setName.split("_")[2],
filter: null,
gain: Number(setName.split("_")[3].replace("gain", "")),
} as ImageSpec);
};
/**
* Detect if ASIAIR directory structure is present.
*/
export const isAsiAirDirectoryF = (
directory: string,
): {
isAsiAirDirectory: boolean;
autorunDirectory: string;
planDirectory: string;
} => {
const autorunDirectory = `${directory}/Autorun`;
const planDirectory = `${directory}/Plan`;
return {
isAsiAirDirectory:
fs.existsSync(autorunDirectory) || fs.existsSync(planDirectory),
autorunDirectory,
planDirectory,
};
};