From 2f5f629e7e5fedc41170efcfdba8e9b5552e1f14 Mon Sep 17 00:00:00 2001
From: Laurent Knoll <laurent.knoll@orange.com>
Date: Sun, 25 Sep 2022 23:03:25 +0200
Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20du=20masque=20du=20blob.=20?=
 =?UTF-8?q?Closes=20#3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/data/coords/transform/toEllipseFitter.ts | 11 +--
 src/data/dataExporter.ts                     | 87 +++++++++++++++++++-
 src/render/ImageDataWrapper.ts               | 79 ++++++++++++++++++
 src/steps/downloadStep.tsx                   | 31 +++----
 4 files changed, 183 insertions(+), 25 deletions(-)
 create mode 100644 src/render/ImageDataWrapper.ts

diff --git a/src/data/coords/transform/toEllipseFitter.ts b/src/data/coords/transform/toEllipseFitter.ts
index ce51ac8..62f08d1 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 7872479..3f7c1f4 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 0000000..7c21c6e
--- /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 9a56ed0..753bf68 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();
     }
 
     /**
-- 
GitLab