diff --git a/src/data/coords/transform/toEllipseFitter.ts b/src/data/coords/transform/toEllipseFitter.ts index ce51ac8a15cc9425b884d669b72952d8d346e78d..62f08d140ceacc7a378bd47034339e29970e1062 100644 --- a/src/data/coords/transform/toEllipseFitter.ts +++ b/src/data/coords/transform/toEllipseFitter.ts @@ -8,6 +8,7 @@ import {types} from "sass"; import Color = types.Color; import {Transformation} from "./transformation"; import {PathCoords} from "../pathCoords"; +import {ImageDataWrapper} from "../../../render/ImageDataWrapper"; const HALFPI : number= 1.5707963267949; /** @@ -141,20 +142,16 @@ export class ToEllipseFitter implements Transformation<PathCoords, Coords>{ path.fillColor = new paper.Color("red"); path.strokeColor = null; const raster = path.rasterize({insert: false}); - const imageData = raster.getImageData(new paper.Rectangle(0, 0, raster.width, raster.height)); - const data = imageData.data; + const pixelRatio = paper.view.pixelRatio; + const rasterDataAccess = ImageDataWrapper.forRaster(raster); for (let y = 0; y < this.height; y++) { bitcountOfLine = 0; xSumOfLine = 0; let offset = Math.round(y * pixelRatio) * raster.width * 4; for (let x=0; x < this.width; x++) { - - // let point = new paper.Point(x + this.left, y + this.top) - // if (point.getDistance(center) < minCenterDist - // || path.contains(point)) { - if(data[data.byteOffset + offset + Math.round(x * pixelRatio) * 4] > 0 ) { + if(rasterDataAccess.getRgbaAt(Math.round(x * pixelRatio), Math.round(y * pixelRatio)).r > 0) { bitcountOfLine++; xSumOfLine += x; this.x2sum += x * x; diff --git a/src/data/dataExporter.ts b/src/data/dataExporter.ts index 78724798929f0a505da4cd15aa726103b037de23..3f7c1f45544f2ada5dfe88cd5d7344aeb43a5078 100644 --- a/src/data/dataExporter.ts +++ b/src/data/dataExporter.ts @@ -10,6 +10,75 @@ import {PathCoords} from "./coords/pathCoords"; const ROUNDING_DECIMALS = 3; +/** + * Merge des point alignés horizontalement ou verticalement + */ +class PointMerger { + + private startPoint: paper.Point | null; + private endPoint: paper.Point | null; + + next(point : paper.Point | null) : paper.Point[] { + if(point == null) { // Null lorsque l'on a passé le dernier point => lus de point à merger, on retourne ce qu'il reste + if(this.endPoint != null) { + return [this.startPoint, this.endPoint]; + } else if(this.startPoint != null) { + return [this.startPoint]; + } else { + return []; + } + } else { + if (this.startPoint == null) { // On est sur le premier point + this.startPoint = point; + return []; + } else { + if(this.endPoint == null) { // On a déjà un point de départ + if(point.y == this.startPoint.y && point.x == this.startPoint.x) { + return [] + } else if(point.y == this.startPoint.y + || point.x == this.startPoint.x) { + this.endPoint = point; + return []; + } else { + let result = [this.startPoint]; + this.startPoint = point; + return result + } + } else { + if(point.y == this.endPoint.y && point.x == this.endPoint.x) { + return []; + + } else if(point.y == this.endPoint.y && Math.sign(this.endPoint.x - this.startPoint.x) == Math.sign(this.endPoint.x - this.startPoint.x)) { + // Dans l'alignement horizontal + this.endPoint = point; + return []; + + } else if(point.x == this.endPoint.x && Math.sign(this.endPoint.y - this.startPoint.y) == Math.sign(this.endPoint.y - this.startPoint.y)) { + // Dans l'alignement vertical + this.endPoint = point; + return []; + + } else { + if(point.y == this.endPoint.y || point.x == this.endPoint.x) { + let result = [this.startPoint]; + this.startPoint = this.endPoint; + this.endPoint = point; + return result; + + } else { + let result = [this.startPoint, this.endPoint]; + this.startPoint = point; + this.endPoint = null; + return result; + } + } + } + } + } + } +} + + export class DataExporter { /** @@ -18,23 +87,29 @@ export class DataExporter { public exportPathPointsAsXYCsv(coords : Coords, close : boolean) : string { const path = coords.toRemovedPath(); let data = ""; + let pointMerger: PointMerger = new PointMerger(); for (let i = 0; i < path.length; i++) { - let point = path.getPointAt(i); - data += Math.round(point.x) + "\t" + Math.round(point.y) + "\n"; + let point = path.getPointAt(i).round(); + pointMerger.next(point).forEach( + (p) => data += this.toCsv(p) + ); } + pointMerger.next(null).forEach( + (p) => data += this.toCsv(p) + ); return data; } - /** * Transforme un Path en CSV par segments */ public exportPathSegmentsAsXYCsv(coords : Coords, close : boolean) : string { const path = coords.toRemovedPath(); let data = ""; + let pointMerger: PointMerger = new PointMerger(); 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"; + data += this.toCsv(segment.point.round()); } return data; } @@ -75,4 +150,8 @@ export class DataExporter { } + private toCsv(point : paper.Point) : string { + return point.x + "\t" + point.y + "\n"; + } + } \ No newline at end of file diff --git a/src/render/ImageDataWrapper.ts b/src/render/ImageDataWrapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c21c6ed49014ca77d535209a9c9a6ebe4c5e85a --- /dev/null +++ b/src/render/ImageDataWrapper.ts @@ -0,0 +1,79 @@ +import * as paper from "paper"; + +/** + * Décrit les composantes RGB + alpha (0 transparent => 1, 255 => opaque) + */ +export interface Rgba { r : number, g : number, b : number, a : number } + +/** + * Accès direct aux données sous-jacente à un raster (Canva/2d context et image data) + */ +export class ImageDataWrapper { + + /** + * Factory method autour d'un Raster + */ + public static forRaster(raster : paper.Raster) : ImageDataWrapper { + return new ImageDataWrapper(raster.getImageData(new paper.Rectangle(0, 0, raster.width, raster.height))); + } + + constructor(public imageData : ImageData) { + } + + /** + * Donne les données RGBA (alpha) aux coordonnées données + */ + getRgbaAt(x: number, y: number) : Rgba { + return this.getRgabAtOffset(this.offset(x, y)); + } + + /** + * Donne les données RGBA (alpha) aux coordonnées données + */ + setRgbaAt(x: number, y: number, rgba : Rgba) : void { + this.setRgabAtOffset(this.offset(x, y), rgba); + } + + /** + * Définit le Rgba à l'offset + */ + private setRgabAtOffset(offset : number, rgba : Rgba) { + this.imageData.data[offset] = rgba.r; + this.imageData.data[offset + 1] = rgba.g; + this.imageData.data[offset + 2] = rgba.b; + this.imageData.data[offset + 3] = rgba.a; + } + + /** + * Donne le Rgba à l'offset + */ + private getRgabAtOffset(offset : number) : Rgba{ + return { r: this.imageData.data[offset], g: this.imageData.data[offset + 1], b: this.imageData.data[offset + 2], a: this.imageData.data[offset + 3] }; + } + + /** + * Donne l'offset dans l'ImageData + */ + private offset(x: number, y: number) : number { + return this.imageData.data.byteOffset + (y * this.imageData.width) * 4 + (x * 4); + } + + /** + * Applique une fonction (comme un filtre) à chaque pixel + */ + apply(f : (Rgba) => Rgba) : ImageDataWrapper { + + for (let offset = this.imageData.data.byteOffset; offset < this.imageData.data.length; offset = offset + 4) { + this.setRgabAtOffset(offset, f(this.getRgabAtOffset(offset))); + } + return this; + } + + /** + * Remplace l'ImageData dans la cible + */ + andStore(canvasRenderingContext2D : CanvasRenderingContext2D) { + canvasRenderingContext2D.putImageData(this.imageData, 0, 0); + } + +} \ No newline at end of file diff --git a/src/steps/downloadStep.tsx b/src/steps/downloadStep.tsx index 9a56ed09bd7a64f51ae82b6812b03c5d1cea02c9..753bf6883bc9489e11f2b2a9c1693f947a4c63a1 100644 --- a/src/steps/downloadStep.tsx +++ b/src/steps/downloadStep.tsx @@ -4,6 +4,7 @@ import {Alert, Button, InputGroup} from "react-bootstrap"; import {IoUtils} from "../utils/ioUtils"; import {DataExporter} from "../data/dataExporter"; import * as paper from "paper"; +import {ImageDataWrapper, Rgba} from "../render/ImageDataWrapper"; export interface DownloadButtonProps { downloading: boolean, @@ -91,21 +92,13 @@ export class DownloadStep extends Step<DownloadStepState> { * Téléchargement des données du mask */ private downloadBlobMask() : void { - let renderingGroup = new paper.Group(); - let background = new paper.Path.Rectangle(new paper.Point(0,0), this.props.lab.data.pictureSize); - background.fillColor = new paper.Color("black"); - background.strokeColor = null; - - renderingGroup.addChild(background) - let path = this.props.lab.data.blobMaskCoords.toRemovedPath(); path.closed = true; path.fillColor = new paper.Color("white"); path.strokeColor = null; - renderingGroup.addChild(path) - renderingGroup.scale(1 / paper.view.pixelRatio); + path.scale(1 / paper.view.pixelRatio, path.bounds.point); - let raster = renderingGroup.rasterize({ insert: false}); + let raster = path.rasterize({ insert: false}); raster.smoothing = "off"; var newCanvas = document.createElement('canvas'); @@ -113,15 +106,25 @@ export class DownloadStep extends Step<DownloadStepState> { newCanvas.width = w; const h = this.props.lab.data.pictureSize.height; newCanvas.height = h; + var newContext = newCanvas.getContext('2d'); - newContext.drawImage(raster.canvas, 0, 0, w, h, 0, 0, w, h); + newContext.fillStyle = "black"; + newContext.fillRect(0, 0, newCanvas.width, newCanvas.height); + newContext.drawImage(raster.canvas, path.bounds.x - 0.5, path.bounds.y - 0.5); + + // Re-aliasing + const imageDataWrapper = new ImageDataWrapper(newContext.getImageData(0, 0, w, h)); + + // Supprime les nuances de gris en se basant sur la valeur rouge (on est sur d'avoir du gris) + imageDataWrapper.apply((rgba: Rgba) => { + let l = rgba.r >= 128 ? 255 : 0; + // let l = rgba.r == 255 ? 255 : 0; + return {r: l, g: l, b: l, a: 255}; + }).andStore(newContext); - // raster.getSubRaster(new paper.Rectangle(new paper.Point(0,0), this.props.lab.data.pictureSize)); IoUtils.downloadDataUrl(this.state.blobMaskFilename, newCanvas.toDataURL("image/png")); newCanvas.remove(); - - renderingGroup.remove(); } /**