Skip to content
Snippets Groups Projects
Commit 8cc83e1a authored by Laurent Knoll's avatar Laurent Knoll
Browse files

Ajout d'un mode debug pour injecter des coordonnées de ImageJ. Calcul du...

Ajout d'un mode debug pour injecter des coordonnées de ImageJ. Calcul du convex hull et de la solidité.
parent 6aaa463f
No related branches found
No related tags found
No related merge requests found
Showing with 274 additions and 58 deletions
......@@ -14,7 +14,7 @@ export class CircleCoords implements Coords {
return new paper.Rectangle(this.center.x - this.radius, this.center.y - this.radius, this.radius * 2, this.radius * 2);
}
toPath(): paper.Path {
toRemovedPath(): paper.Path {
return new paper.Path.Circle(this.center, this.radius);
}
......
import * as paper from "paper";
import {PathCoords} from "./pathCoords";
/**
......@@ -15,5 +16,6 @@ export interface Coords {
/**
* Un paper.Path representatif des coodonnées
*/
toPath() : paper.Path;
toRemovedPath() : paper.Path;
}
\ No newline at end of file
......@@ -10,10 +10,10 @@ export class EllipseCoords implements Coords {
}
bounds(): paper.Rectangle {
return this.toPath().bounds;
return this.toRemovedPath().bounds;
}
toPath(): paper.Path {
toRemovedPath(): paper.Path {
let path = new paper.Path.Ellipse(new paper.Rectangle(
new paper.Point(this.center.x - this.radiusX, this.center.y - this.radiusY),
new paper.Size(2 * this.radiusX, 2 * this.radiusY)
......
......@@ -11,26 +11,30 @@ const CLOSING_DISTANCE = 10;
*/
export class PathCoords implements Coords {
public constructor(public path : paper.Path) {
public constructor(public points : paper.Point[] = []) {
}
bounds(): paper.Rectangle {
return this.path.bounds;
return this.toRemovedPath().bounds;
}
toPath(): paper.Path {
const result = this.path.clone();
result.remove();
return result;
toRemovedPath(): paper.Path {
let path = new paper.Path();
path.remove();
this.points.forEach((point) => {
path.add(point)
})
return path;
}
/**
* Indique si le masque est fermé (le dernier point est proche du premier)
*/
public isClosed() : boolean {
return !this.path.isEmpty()
&& (this.path.bounds.width > CLOSING_DISTANCE || this.path.bounds.height > CLOSING_DISTANCE)
&& this.path.firstSegment.point.getDistance(this.path.lastSegment.point) < CLOSING_DISTANCE;
let path = this.toRemovedPath();
return !path.isEmpty()
&& (path.bounds.width > CLOSING_DISTANCE || path.bounds.height > CLOSING_DISTANCE)
&& path.firstSegment.point.getDistance(path.lastSegment.point) < CLOSING_DISTANCE;
}
}
\ No newline at end of file
import {PathCoords} from "../pathCoords";
import {Transformation} from "./transformation";
enum Orientation {
Collinear = 0,
Clockwise = 1,
Counterclockwise= 2
}
/**
* Génère une coque convexe
* Jarvis’s Algorithm or Wrapping
* @see https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
*/
export class ToConvexHull implements Transformation<PathCoords, PathCoords> {
transform(pathCoords: PathCoords): PathCoords {
return new PathCoords(this.convexHull(pathCoords.points));
}
/**
*
* Prints convex hull of a set of n points.
*/
private convexHull(points : paper.Point[]) : paper.Point[] {
if (points.length < 3) {
return points;
}
// Initialize Result
let hull = [];
// Find the leftmost point
let startIndex = 0;
for (let i = 1; i < points.length; i++) {
if (points[i].x < points[startIndex].x) {
startIndex = i;
}
}
// Start from leftmost point, keep moving
// counterclockwise until reach the start point
// again. This loop runs O(h) times where h is
// number of points in result or output.
let p = startIndex;
do {
// Add current point to result
hull.push(points[p]);
// Search for a point 'q' such that
// orientation(p, q, x) is counterclockwise
// for all points 'x'. The idea is to keep
// track of last visited most counterclock-
// wise point in q. If any point 'i' is more
// counterclock-wise than q, then update q.
let q = (p + 1) % points.length;
for (let i = 0; i < points.length; i++) {
// If i is more counterclockwise than
// current q, then update q
if (this.orientation(points[p], points[i], points[q]) == Orientation.Counterclockwise) {
q = i;
}
}
// Now q is the most counterclockwise with
// respect to p. Set p as q for next iteration,
// so that q is added to result 'hull'
p = q;
} while (p != startIndex); // While we don't come to first point
return hull;
}
/**
* To find orientation of ordered triplet (p, q, r)
*/
private orientation(p : paper.Point, q : paper.Point, r : paper.Point) : Orientation {
let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
if (val == 0) {
return Orientation.Collinear;
} else if (val > 0) {
return Orientation.Clockwise;
} else {
return Orientation.Counterclockwise;
}
}
}
\ No newline at end of file
......@@ -13,7 +13,7 @@ const HALFPI : number= 1.5707963267949;
/**
* Adaptation de EllipseFitter.java de ImageJ
*/
export class EllipseFitter implements Transformation<PathCoords, Coords>{
export class ToEllipseFitter implements Transformation<PathCoords, Coords>{
private bitCount : number = 0;
private xsum : number = 0;
......@@ -54,7 +54,7 @@ export class EllipseFitter implements Transformation<PathCoords, Coords>{
let theta : number;
const path = from.toPath();
const path = from.toRemovedPath();
let bounds = path.bounds;
this.left = Math.round(bounds.x);
this.top = Math.round(bounds.y);
......@@ -113,7 +113,7 @@ export class EllipseFitter implements Transformation<PathCoords, Coords>{
xCenter = this.left + xoffset + 0.5;
yCenter = this.top + yoffset + 0.5;
return new EllipseCoords(new paper.Point(xCenter, yCenter), major / 2, minor / 2, -angle);
return new EllipseCoords(new paper.Point(xCenter, yCenter), major / 2, minor / 2, angle);
}
private computeSums (path : paper.Path) : void {
......
......@@ -22,7 +22,7 @@ export class VectorCoords implements Coords {
return this.end.subtract(this.start);
}
toPath(): paper.Path {
toRemovedPath(): paper.Path {
return new paper.Path.Line(this.start, this.end);
}
......
......@@ -2,9 +2,11 @@
* Utilitaire d'export des données
*/
import {LabData} from "../lab";
import {EllipseFitter} from "./coords/transform/ellipseFitter";
import {ToEllipseFitter} from "./coords/transform/toEllipseFitter";
import {Coords} from "./coords/coords";
import {MathUtils} from "../utils/mathUtils";
import {ToConvexHull} from "./coords/transform/toConvexHull";
import {PathCoords} from "./coords/pathCoords";
const ROUNDING_DECIMALS = 3;
......@@ -14,14 +16,11 @@ export class DataExporter {
* Transforme un Path en CSV
*/
public exportPathAsXYCsv(coords : Coords, close : boolean) : string {
const path = coords.toPath();
if(close) {
path.closePath();
}
const path = coords.toRemovedPath();
let data = "";
for (let i = 0; i < path.length; i++) {
let point = path.getPointAt(i);
data += Math.round(point.x) + "\t" + Math.round(point.y) + "\n";
for (let i = 0; i < path.segments.length; i++) {
let segment = path.segments[i];
data += Math.round(segment.point.x) + "\t" + Math.round(segment.point.y) + "\n";
}
return data;
}
......@@ -29,13 +28,14 @@ export class DataExporter {
/**
* Transforme un Path en CSV
*/
public exportPathDescriptorsAsCsv(labData : LabData, coords : Coords) : string {
let path = coords.toPath();
public exportPathDescriptorsAsCsv(labData : LabData, coords : PathCoords) : string {
let path = coords.toRemovedPath();
path.closePath();
let linearScale = labData.rulerCoords.distance() / labData.rulerTickCount; // pixels/cm
let areaScale = Math.pow(linearScale, 2);
let fittingEllipse = new EllipseFitter().transform(coords);
let fittingEllipse = new ToEllipseFitter().transform(coords);
let convexHull = new ToConvexHull().transform(coords);
let line = 1;
let label = labData.filename;
......@@ -44,7 +44,7 @@ export class DataExporter {
let circularity = 4 * Math.PI * area / Math.pow(perimeter, 2);
let ar = fittingEllipse.getMajorAxis() / fittingEllipse.getMinorAxis();
let round = 4 * area / (Math.PI * fittingEllipse.getMajorAxis());
let solid = "todo";
let solid = area / (convexHull.toRemovedPath().area / areaScale);
let headers : string[] = [ " ", "Label", "Area", "Perim.", "Circ.","AR","Round","Solidity"];
let data = [ line,
......@@ -54,7 +54,7 @@ export class DataExporter {
MathUtils.round(circularity, ROUNDING_DECIMALS),
MathUtils.round(ar, ROUNDING_DECIMALS),
MathUtils.round(round, ROUNDING_DECIMALS),
solid,
MathUtils.round(solid, ROUNDING_DECIMALS),
]
return headers.join(",") + "\n" + data.join(",");
......
......@@ -2,22 +2,29 @@ import * as paper from "paper";
import {AbstractInstrument, Handle, Instrument} from "./instrument";
import {PathCoords} from "../data/coords/pathCoords";
import {DEBUG_MODE, Lab} from "../lab";
import {EllipseFitter} from "../data/coords/transform/ellipseFitter";
import {ToEllipseFitter} from "../data/coords/transform/toEllipseFitter";
import {PaperUtils} from "../utils/paperUtils";
import {ToConvexHull} from "../data/coords/transform/toConvexHull";
/**
* Représente la boîte de Petri
*/
export class BlobMask extends AbstractInstrument<PathCoords> implements Instrument {
/**
* Garde l'état fermé ou pas
*/
private wasClosed: boolean = false;
/**
* Appelé lorsque le tracé est fermé
*/
public onClosed : () => void = () => {};
public onClose : () => void = () => {};
/**
* Appelé lorsque le tracé s'ouvre
*/
public onOpened : () => void = () => {};
public onOpen : () => void = () => {};
public constructor(protected lab : Lab, coords : PathCoords) {
super(lab, coords, [
......@@ -26,14 +33,38 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume
}
drawIn(coords: PathCoords, group: paper.Group) {
let line = coords.path.clone();
let line = coords.toRemovedPath();
group.addChild(line);
}
/**
* Surcharge pour gérer l'état fermé ouvert du tracé
*/
refresh() {
super.refresh();
let isClosed = this.coords.isClosed();
if(!this.wasClosed && isClosed) {
this.onClose();
} else if(this.wasClosed && !isClosed) {
this.onOpen();
}
this.wasClosed = isClosed;
if(DEBUG_MODE) {
if(line.length > 10) {
const pathCoords = new PathCoords(line.clone());
group.addChild(new EllipseFitter().transform(pathCoords).toPath());
if(this.drawGroup.bounds.area > 10) {
let fittedEllipse = new ToEllipseFitter().transform(this.coords).toRemovedPath();
fittedEllipse.strokeColor = new paper.Color("red");
fittedEllipse.strokeWidth = PaperUtils.absoluteDimension(2);
fittedEllipse.dashArray = [10, 12];
this.drawGroup.addChild(fittedEllipse);
}
let convexHull = new ToConvexHull().transform(this.coords).toRemovedPath();
convexHull.strokeColor = new paper.Color("red");
convexHull.strokeWidth = PaperUtils.absoluteDimension(2);
convexHull.dashArray = [10, 12];
this.drawGroup.addChild(convexHull);
}
}
......@@ -44,10 +75,10 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume
locateHandle(coords: PathCoords, handle: Handle): paper.Point {
if(handle.name == "startHandle") {
if(coords.path.isEmpty()) {
if(coords.points.length == 0) {
return null;
} else {
return coords.path.firstSegment.point;
return coords.points[0];
}
}
throw new Error("Unknown handle");
......@@ -71,12 +102,8 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume
return true;
}
if(!this.coords.isClosed()) { // Une fois la boucle fermée, on ne peut plus ajouter de
this.coords.path.add(event.point)
if(this.coords.isClosed()) {
this.onClosed();
}
this.coords.points.push(event.point)
}
this.refresh();
return true;
}
......@@ -95,7 +122,7 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume
* Supprime quelques derniers points
*/
public undo() {
this._undo(Math.max(this.coords.path.segments.length - 5, 0));
this._undo(Math.max(this.coords.points.length - 5, 0));
}
/**
......@@ -109,11 +136,7 @@ export class BlobMask extends AbstractInstrument<PathCoords> implements Instrume
* En charge de la suppression
*/
private _undo(from : number) {
let wasClosed = this.coords.isClosed();
this.coords.path.removeSegments(from);
if(wasClosed) {
this.onOpened();
}
this.coords.points = this.coords.points.slice(0, from);
this.refresh();
}
......
import * as paper from "paper";
import {Lab} from "../lab";
import {DEBUG_MODE, Lab} from "../lab";
import {Coords} from "../data/coords/coords";
import {PaperUtils} from "../utils/paperUtils";
import {ToEllipseFitter} from "../data/coords/transform/toEllipseFitter";
/**
* Un instrument d'analyse
......@@ -59,7 +60,7 @@ export abstract class AbstractInstrument<C extends Coords> implements Instrument
/**
* Contient le dessin de l'instrument
*/
private drawGroup : paper.Group = new paper.Group();
protected drawGroup : paper.Group = new paper.Group();
protected constructor(protected lab : Lab, protected coords : C, protected handles : Handle[]) {
}
......
......@@ -18,7 +18,7 @@ export class PetriDish extends AbstractInstrument<EllipseCoords> implements Inst
}
drawIn(coords : EllipseCoords, group: paper.Group) {
group.addChild(coords.toPath());
group.addChild(coords.toRemovedPath());
}
onHandleMove(coords: EllipseCoords, handle: Handle, point: paper.Point, delta: paper.Point): void {
......
......@@ -18,10 +18,9 @@ import {EllipseCoords} from "./data/coords/ellipseCoords";
/**
* Debug mode (ou pas)
*/
export const DEBUG_MODE = false;
export const DEBUG_MODE = true;
export interface LabData {
pictureSize : paper.Size,
filename : string,
......@@ -144,7 +143,6 @@ export class Lab extends React.Component<{}> {
let width = image.naturalWidth;
let height = image.naturalHeight;
this.data = {
pictureSize: new paper.Size(width, height),
filename: filename,
......@@ -155,8 +153,7 @@ export class Lab extends React.Component<{}> {
petriDishCoords: new EllipseCoords(new paper.Point(width / 2, height / 2), width * 0.75 / 2, width * 0.75 / 2, 0),
blobMaskCoords: new PathCoords(new paper.Path()),
blobMaskCoords: new PathCoords(),
};
// Le plus en dessous en premier
......@@ -258,9 +255,9 @@ export class Lab extends React.Component<{}> {
* Appelé lorsque le zoom a changé
*/
private onZoomChanged() {
this.ruler.refresh();
this.petriDish.refresh();
this.blobMask.refresh();
this.ruler?.refresh();
this.petriDish?.refresh();
this.blobMask?.refresh();
}
render(): React.ReactNode {
......
......@@ -83,10 +83,8 @@ export class DownloadStep extends Step<DownloadStepState> {
* Téléchargement des données du mask
*/
private downloadBlobMaskData() : void {
let path = new paper.Path(this.props.lab.data.blobMaskCoords.path.segments);
path.closed = true; // ferme le contour
let data = this.dataExporter.exportPathAsXYCsv(this.props.lab.data.blobMaskCoords, true);
IoUtils.downloadData(this.state.petriDishDataFilename, "text/plain;charset=UTF-8", data);
IoUtils.downloadData(this.state.blobMaskDataFilename, "text/plain;charset=UTF-8", data);
}
/**
......@@ -100,7 +98,7 @@ export class DownloadStep extends Step<DownloadStepState> {
renderingGroup.addChild(background)
let path = this.props.lab.data.blobMaskCoords.toPath();
let path = this.props.lab.data.blobMaskCoords.toRemovedPath();
path.closed = true;
path.fillColor = new paper.Color("white");
path.strokeColor = null;
......
import {Step, StepProps, StepState} from "./step";
import * as React from "react";
import * as paper from "paper";
import {Alert, Button} from "react-bootstrap";
import {IoUtils} from "../utils/ioUtils";
import {DEBUG_MODE} from "../lab";
import {StringUtils} from "../utils/stringUtils";
interface DrawBlobMaskStepState extends StepState {
......@@ -24,10 +28,10 @@ export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> {
onActivation(): void {
this.props.lab.blobMask.activate();
this.props.lab.blobMask.onClosed = () => {
this.props.lab.blobMask.onClose = () => {
this.setState({closed: true });
};
this.props.lab.blobMask.onOpened = () => {
this.props.lab.blobMask.onOpen = () => {
this.setState({closed: false });
};
this.props.lab.zoomOn( this.props.lab.data.petriDishCoords.bounds(), 0.05);
......@@ -37,6 +41,23 @@ export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> {
this.props.lab.blobMask.deactivate();
}
loadData(): void {
IoUtils.openTextFile(
(text : string) => {
console.info("text loaded")
this.props.lab.data.blobMaskCoords.points = [];
let lines = StringUtils.splitLines(text);
lines.filter(line => line.trim().length > 0).forEach(
(line : string) => {
let [x, y] = line.split("\t");
this.props.lab.data.blobMaskCoords.points.push(new paper.Point(Number(x), Number(y)));
}
);
// this.props.lab.data.blobMaskCoords.points.push(this.props.lab.data.blobMaskCoords.points[0]);
this.props.lab.blobMask.refresh();
});
}
render() : React.ReactNode {
return <div>
<div>
......@@ -49,6 +70,13 @@ export class DrawBlobMaskStep extends Step<DrawBlobMaskStepState> {
<Button className={"col-3"} variant={"success"} disabled={!this.state.active || !this.state.closed} onClick={this.terminate.bind(this)}>
<span hidden={!this.state.closed}><i className="fa-solid fa-hands-clapping fa-beat-fade me-2"></i></span>Fini !
</Button>
{DEBUG_MODE ?
<Button className={"ms-2 col-4"} variant={"danger"} disabled={!this.state.active}
onClick={this.loadData.bind(this)}>
<i className="fa-solid fa-bug"></i> Charger XY
</Button>
: <></>
}
</div>
</div>
}
......
......@@ -3,6 +3,29 @@
*/
export class IoUtils {
/**
* Ouvre un chargement
*/
public static openTextFile(onTextLoad : (text: string) => void) : void {
let input : HTMLInputElement = document.createElement('input');
input.type = 'file';
input.accept = '.txt';
input.onchange = (e: InputEvent) => {
// getting a hold of the file reference
var file = (e.target as HTMLInputElement).files[0];
// setting up the reader
let reader = new FileReader();
reader.readAsText(file,'UTF-8');
// here we tell the reader what to do when it's done reading...
reader.onload = readerEvent => {
onTextLoad(readerEvent.target.result as string);
}
}
input.click();
}
/**
* Donne le nom de fichier sans extension
*/
......
/**
* Utilitaires
*/
export class StringUtils {
/**
* Découpe en multi-ligne
*/
public static splitLines(text: string) : string[] {
return text.split(/\r?\n/);
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment